Merge branch 'master' into beta
4
.github/workflows/build.yml
vendored
@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-11
|
||||
runs-on: macos-12
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -17,7 +17,7 @@ jobs:
|
||||
fetch-depth: '0'
|
||||
|
||||
- name: Set active Xcode path
|
||||
run: sudo xcode-select -s /Applications/Xcode_12.5.1.app/Contents/Developer
|
||||
run: sudo xcode-select -s /Applications/Xcode_13.2.1.app/Contents/Developer
|
||||
|
||||
- name: Create canonical source directory
|
||||
run: |
|
||||
|
||||
@ -28,6 +28,24 @@ internal:
|
||||
- build/artifacts
|
||||
expire_in: 1 week
|
||||
|
||||
appstore_development:
|
||||
tags:
|
||||
- ios_internal
|
||||
stage: build
|
||||
only:
|
||||
- appstore-development
|
||||
except:
|
||||
- tags
|
||||
script:
|
||||
- bash buildbox/build-telegram.sh appstore-development
|
||||
- bash buildbox/deploy-telegram.sh appstore-development
|
||||
environment:
|
||||
name: appstore-development
|
||||
artifacts:
|
||||
paths:
|
||||
- build/artifacts/Telegram.DSYMs.zip
|
||||
expire_in: 1 week
|
||||
|
||||
experimental_i:
|
||||
tags:
|
||||
- ios_internal
|
||||
|
||||
2
.gitmodules
vendored
@ -4,7 +4,7 @@
|
||||
url=../rlottie.git
|
||||
[submodule "build-system/bazel-rules/rules_apple"]
|
||||
path = build-system/bazel-rules/rules_apple
|
||||
url=https://github.com/bazelbuild/rules_apple.git
|
||||
url=https://github.com/ali-fareed/rules_apple.git
|
||||
[submodule "build-system/bazel-rules/rules_swift"]
|
||||
path = build-system/bazel-rules/rules_swift
|
||||
url=https://github.com/bazelbuild/rules_swift.git
|
||||
|
||||
@ -298,6 +298,9 @@ alternate_icon_folders = [
|
||||
"WhiteFilledIcon",
|
||||
"New1",
|
||||
"New2",
|
||||
"Premium",
|
||||
"PremiumBlack",
|
||||
"PremiumTurbo",
|
||||
]
|
||||
|
||||
[
|
||||
@ -329,6 +332,7 @@ swift_library(
|
||||
"Telegram-iOS/Application.swift",
|
||||
]),
|
||||
data = [
|
||||
":Icons",
|
||||
":AppResources",
|
||||
":AppIntentVocabularyResources",
|
||||
":InfoPlistStringResources",
|
||||
@ -337,6 +341,7 @@ swift_library(
|
||||
"//submodules/OverlayStatusController:OverlayStatusControllerResources",
|
||||
"//submodules/PasswordSetupUI:PasswordSetupUIResources",
|
||||
"//submodules/PasswordSetupUI:PasswordSetupUIAssets",
|
||||
"//submodules/PremiumUI:PremiumUIResources",
|
||||
"//submodules/TelegramUI:TelegramUIResources",
|
||||
"//submodules/TelegramUI:TelegramUIAssets",
|
||||
":GeneratedPresentationStrings/Resources/PresentationStrings.data",
|
||||
@ -1797,6 +1802,8 @@ plist_fragment(
|
||||
<string>We need this so that you can share photos and videos from your photo library.</string>
|
||||
<key>NSSiriUsageDescription</key>
|
||||
<string>You can use Siri to send messages.</string>
|
||||
<key>NSSpeechRecognitionUsageDescription</key>
|
||||
<string>We need this to transcribe audio messages on your request.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
|
||||
@ -38,7 +38,7 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi
|
||||
|
||||
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
|
||||
|
||||
self.impl = NotificationViewControllerImpl(initializationData: NotificationViewControllerInitializationData(appBundleId: baseAppBundleId, appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), setPreferredContentSize: { [weak self] size in
|
||||
self.impl = NotificationViewControllerImpl(initializationData: NotificationViewControllerInitializationData(appBundleId: baseAppBundleId, appBuildType: buildConfig.isAppStoreBuild ? .public : .internal, appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), setPreferredContentSize: { [weak self] size in
|
||||
self?.preferredContentSize = size
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
|
||||
objc_library(
|
||||
name = "NotificationServiceObjC",
|
||||
enable_modules = True,
|
||||
module_name = "NotificationServiceObjC",
|
||||
srcs = glob([
|
||||
"Sources/**/*.m",
|
||||
"Sources/**/*.h",
|
||||
]),
|
||||
hdrs = glob([
|
||||
"PublicHeaders/**/*.h",
|
||||
]),
|
||||
includes = [
|
||||
"PublicHeaders",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/BuildConfig:BuildConfig",
|
||||
"//submodules/MtProtoKit:MtProtoKit",
|
||||
"//submodules/NotificationsPresentationData:NotificationsPresentationData",
|
||||
"//submodules/OpenSSLEncryptionProvider:OpenSSLEncryptionProvider",
|
||||
],
|
||||
sdk_frameworks = [
|
||||
"Foundation",
|
||||
],
|
||||
weak_sdk_frameworks = [
|
||||
"BackgroundTasks",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@ -1,17 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
#import <BuildConfig/BuildConfig.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NotificationServiceImpl : NSObject
|
||||
|
||||
- (instancetype)initWithSerialDispatch:(void (^)(dispatch_block_t))serialDispatch countIncomingMessage:(void (^)(NSString *, int64_t, DeviceSpecificEncryptionParameters *, int64_t, int32_t))countIncomingMessage isLocked:(bool (^)(NSString *))isLocked lockedMessageText:(NSString *(^)(NSString *))lockedMessageText;
|
||||
|
||||
- (void)updateUnreadCount:(int32_t)unreadCount;
|
||||
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler;
|
||||
- (void)serviceExtensionTimeWillExpire;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -1,308 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/*
|
||||
* Layer 1
|
||||
*/
|
||||
|
||||
@class Api1_Photo;
|
||||
@class Api1_Photo_photoEmpty;
|
||||
@class Api1_Photo_photo;
|
||||
|
||||
@class Api1_PhotoSize;
|
||||
@class Api1_PhotoSize_photoSizeEmpty;
|
||||
@class Api1_PhotoSize_photoSize;
|
||||
@class Api1_PhotoSize_photoCachedSize;
|
||||
@class Api1_PhotoSize_photoStrippedSize;
|
||||
@class Api1_PhotoSize_photoSizeProgressive;
|
||||
|
||||
@class Api1_FileLocation;
|
||||
@class Api1_FileLocation_fileLocationToBeDeprecated;
|
||||
|
||||
@class Api1_DocumentAttribute;
|
||||
@class Api1_DocumentAttribute_documentAttributeImageSize;
|
||||
@class Api1_DocumentAttribute_documentAttributeAnimated;
|
||||
@class Api1_DocumentAttribute_documentAttributeSticker;
|
||||
@class Api1_DocumentAttribute_documentAttributeVideo;
|
||||
@class Api1_DocumentAttribute_documentAttributeAudio;
|
||||
@class Api1_DocumentAttribute_documentAttributeFilename;
|
||||
@class Api1_DocumentAttribute_documentAttributeHasStickers;
|
||||
|
||||
@class Api1_InputStickerSet;
|
||||
@class Api1_InputStickerSet_inputStickerSetEmpty;
|
||||
@class Api1_InputStickerSet_inputStickerSetID;
|
||||
@class Api1_InputStickerSet_inputStickerSetShortName;
|
||||
|
||||
@class Api1_InputFileLocation;
|
||||
@class Api1_InputFileLocation_inputPhotoFileLocation;
|
||||
@class Api1_InputFileLocation_inputDocumentFileLocation;
|
||||
|
||||
@class Api1_MaskCoords;
|
||||
@class Api1_MaskCoords_maskCoords;
|
||||
|
||||
@class Api1_Document;
|
||||
@class Api1_Document_document;
|
||||
|
||||
|
||||
@interface Api1__Environment : NSObject
|
||||
|
||||
+ (NSData *)serializeObject:(id)object;
|
||||
+ (id)parseObject:(NSData *)data;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_FunctionContext : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData *payload;
|
||||
@property (nonatomic, copy, readonly) id (^responseParser)(NSData *);
|
||||
@property (nonatomic, strong, readonly) id metadata;
|
||||
|
||||
- (instancetype)initWithPayload:(NSData *)payload responseParser:(id (^)(NSData *))responseParser metadata:(id)metadata;
|
||||
|
||||
@end
|
||||
|
||||
/*
|
||||
* Types 1
|
||||
*/
|
||||
|
||||
@interface Api1_Photo : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * pid;
|
||||
|
||||
+ (Api1_Photo_photoEmpty *)photoEmptyWithPid:(NSNumber *)pid;
|
||||
+ (Api1_Photo_photo *)photoWithFlags:(NSNumber *)flags pid:(NSNumber *)pid accessHash:(NSNumber *)accessHash fileReference:(NSData *)fileReference date:(NSNumber *)date sizes:(NSArray *)sizes dcId:(NSNumber *)dcId;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_Photo_photoEmpty : Api1_Photo
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_Photo_photo : Api1_Photo
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * flags;
|
||||
@property (nonatomic, strong, readonly) NSNumber * accessHash;
|
||||
@property (nonatomic, strong, readonly) NSData * fileReference;
|
||||
@property (nonatomic, strong, readonly) NSNumber * date;
|
||||
@property (nonatomic, strong, readonly) NSArray * sizes;
|
||||
@property (nonatomic, strong, readonly) NSNumber * dcId;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_PhotoSize : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString * type;
|
||||
|
||||
+ (Api1_PhotoSize_photoSizeEmpty *)photoSizeEmptyWithType:(NSString *)type;
|
||||
+ (Api1_PhotoSize_photoSize *)photoSizeWithType:(NSString *)type location:(Api1_FileLocation *)location w:(NSNumber *)w h:(NSNumber *)h size:(NSNumber *)size;
|
||||
+ (Api1_PhotoSize_photoCachedSize *)photoCachedSizeWithType:(NSString *)type location:(Api1_FileLocation *)location w:(NSNumber *)w h:(NSNumber *)h bytes:(NSData *)bytes;
|
||||
+ (Api1_PhotoSize_photoStrippedSize *)photoStrippedSizeWithType:(NSString *)type bytes:(NSData *)bytes;
|
||||
+ (Api1_PhotoSize_photoSizeProgressive *)photoSizeProgressiveWithType:(NSString *)type location:(Api1_FileLocation *)location w:(NSNumber *)w h:(NSNumber *)h sizes:(NSArray *)sizes;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_PhotoSize_photoSizeEmpty : Api1_PhotoSize
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_PhotoSize_photoSize : Api1_PhotoSize
|
||||
|
||||
@property (nonatomic, strong, readonly) Api1_FileLocation * location;
|
||||
@property (nonatomic, strong, readonly) NSNumber * w;
|
||||
@property (nonatomic, strong, readonly) NSNumber * h;
|
||||
@property (nonatomic, strong, readonly) NSNumber * size;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_PhotoSize_photoCachedSize : Api1_PhotoSize
|
||||
|
||||
@property (nonatomic, strong, readonly) Api1_FileLocation * location;
|
||||
@property (nonatomic, strong, readonly) NSNumber * w;
|
||||
@property (nonatomic, strong, readonly) NSNumber * h;
|
||||
@property (nonatomic, strong, readonly) NSData * bytes;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_PhotoSize_photoStrippedSize : Api1_PhotoSize
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData * bytes;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_PhotoSize_photoSizeProgressive : Api1_PhotoSize
|
||||
|
||||
@property (nonatomic, strong) Api1_FileLocation * location;
|
||||
@property (nonatomic, strong) NSNumber * w;
|
||||
@property (nonatomic, strong) NSNumber * h;
|
||||
@property (nonatomic, strong) NSArray * sizes;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_FileLocation : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * volumeId;
|
||||
@property (nonatomic, strong, readonly) NSNumber * localId;
|
||||
|
||||
+ (Api1_FileLocation_fileLocationToBeDeprecated *)fileLocationToBeDeprecatedWithVolumeId:(NSNumber *)volumeId localId:(NSNumber *)localId;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_FileLocation_fileLocationToBeDeprecated : Api1_FileLocation
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_DocumentAttribute : NSObject
|
||||
|
||||
+ (Api1_DocumentAttribute_documentAttributeImageSize *)documentAttributeImageSizeWithW:(NSNumber *)w h:(NSNumber *)h;
|
||||
+ (Api1_DocumentAttribute_documentAttributeAnimated *)documentAttributeAnimated;
|
||||
+ (Api1_DocumentAttribute_documentAttributeSticker *)documentAttributeStickerWithFlags:(NSNumber *)flags alt:(NSString *)alt stickerset:(Api1_InputStickerSet *)stickerset maskCoords:(Api1_MaskCoords *)maskCoords;
|
||||
+ (Api1_DocumentAttribute_documentAttributeVideo *)documentAttributeVideoWithFlags:(NSNumber *)flags duration:(NSNumber *)duration w:(NSNumber *)w h:(NSNumber *)h;
|
||||
+ (Api1_DocumentAttribute_documentAttributeAudio *)documentAttributeAudioWithFlags:(NSNumber *)flags duration:(NSNumber *)duration title:(NSString *)title performer:(NSString *)performer waveform:(NSData *)waveform;
|
||||
+ (Api1_DocumentAttribute_documentAttributeFilename *)documentAttributeFilenameWithFileName:(NSString *)fileName;
|
||||
+ (Api1_DocumentAttribute_documentAttributeHasStickers *)documentAttributeHasStickers;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeImageSize : Api1_DocumentAttribute
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * w;
|
||||
@property (nonatomic, strong, readonly) NSNumber * h;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeAnimated : Api1_DocumentAttribute
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeSticker : Api1_DocumentAttribute
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * flags;
|
||||
@property (nonatomic, strong, readonly) NSString * alt;
|
||||
@property (nonatomic, strong, readonly) Api1_InputStickerSet * stickerset;
|
||||
@property (nonatomic, strong, readonly) Api1_MaskCoords * maskCoords;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeVideo : Api1_DocumentAttribute
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * flags;
|
||||
@property (nonatomic, strong, readonly) NSNumber * duration;
|
||||
@property (nonatomic, strong, readonly) NSNumber * w;
|
||||
@property (nonatomic, strong, readonly) NSNumber * h;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeAudio : Api1_DocumentAttribute
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * flags;
|
||||
@property (nonatomic, strong, readonly) NSNumber * duration;
|
||||
@property (nonatomic, strong, readonly) NSString * title;
|
||||
@property (nonatomic, strong, readonly) NSString * performer;
|
||||
@property (nonatomic, strong, readonly) NSData * waveform;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeFilename : Api1_DocumentAttribute
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString * fileName;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_DocumentAttribute_documentAttributeHasStickers : Api1_DocumentAttribute
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_InputStickerSet : NSObject
|
||||
|
||||
+ (Api1_InputStickerSet_inputStickerSetEmpty *)inputStickerSetEmpty;
|
||||
+ (Api1_InputStickerSet_inputStickerSetID *)inputStickerSetIDWithPid:(NSNumber *)pid accessHash:(NSNumber *)accessHash;
|
||||
+ (Api1_InputStickerSet_inputStickerSetShortName *)inputStickerSetShortNameWithShortName:(NSString *)shortName;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_InputStickerSet_inputStickerSetEmpty : Api1_InputStickerSet
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_InputStickerSet_inputStickerSetID : Api1_InputStickerSet
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * pid;
|
||||
@property (nonatomic, strong, readonly) NSNumber * accessHash;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_InputStickerSet_inputStickerSetShortName : Api1_InputStickerSet
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString * shortName;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_InputFileLocation : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * pid;
|
||||
@property (nonatomic, strong, readonly) NSNumber * accessHash;
|
||||
@property (nonatomic, strong, readonly) NSData * fileReference;
|
||||
@property (nonatomic, strong, readonly) NSString * thumbSize;
|
||||
|
||||
+ (Api1_InputFileLocation_inputPhotoFileLocation *)inputPhotoFileLocationWithPid:(NSNumber *)pid accessHash:(NSNumber *)accessHash fileReference:(NSData *)fileReference thumbSize:(NSString *)thumbSize;
|
||||
+ (Api1_InputFileLocation_inputDocumentFileLocation *)inputDocumentFileLocationWithPid:(NSNumber *)pid accessHash:(NSNumber *)accessHash fileReference:(NSData *)fileReference thumbSize:(NSString *)thumbSize;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_InputFileLocation_inputPhotoFileLocation : Api1_InputFileLocation
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_InputFileLocation_inputDocumentFileLocation : Api1_InputFileLocation
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_MaskCoords : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * n;
|
||||
@property (nonatomic, strong, readonly) NSNumber * x;
|
||||
@property (nonatomic, strong, readonly) NSNumber * y;
|
||||
@property (nonatomic, strong, readonly) NSNumber * zoom;
|
||||
|
||||
+ (Api1_MaskCoords_maskCoords *)maskCoordsWithN:(NSNumber *)n x:(NSNumber *)x y:(NSNumber *)y zoom:(NSNumber *)zoom;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_MaskCoords_maskCoords : Api1_MaskCoords
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface Api1_Document : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSNumber * flags;
|
||||
@property (nonatomic, strong, readonly) NSNumber * pid;
|
||||
@property (nonatomic, strong, readonly) NSNumber * accessHash;
|
||||
@property (nonatomic, strong, readonly) NSData * fileReference;
|
||||
@property (nonatomic, strong, readonly) NSNumber * date;
|
||||
@property (nonatomic, strong, readonly) NSString * mimeType;
|
||||
@property (nonatomic, strong, readonly) NSNumber * size;
|
||||
@property (nonatomic, strong, readonly) NSArray * thumbs;
|
||||
@property (nonatomic, strong, readonly) NSNumber * dcId;
|
||||
@property (nonatomic, strong, readonly) NSArray * attributes;
|
||||
|
||||
+ (Api1_Document_document *)documentWithFlags:(NSNumber *)flags pid:(NSNumber *)pid accessHash:(NSNumber *)accessHash fileReference:(NSData *)fileReference date:(NSNumber *)date mimeType:(NSString *)mimeType size:(NSNumber *)size thumbs:(NSArray *)thumbs dcId:(NSNumber *)dcId attributes:(NSArray *)attributes;
|
||||
|
||||
@end
|
||||
|
||||
@interface Api1_Document_document : Api1_Document
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/*
|
||||
* Functions 1
|
||||
*/
|
||||
|
||||
@interface Api1: NSObject
|
||||
|
||||
@end
|
||||
@ -1,7 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
id _Nullable parseAttachment(NSData * _Nonnull data);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -1,31 +0,0 @@
|
||||
#import "Attachments.h"
|
||||
|
||||
#import <MTProtoKit/MTProtoKit.h>
|
||||
|
||||
#import "Api.h"
|
||||
|
||||
id _Nullable parseAttachment(NSData * _Nonnull data) {
|
||||
if (data.length < 4) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
MTInputStream *inputStream = [[MTInputStream alloc] initWithData:data];
|
||||
|
||||
int32_t signature = [inputStream readInt32];
|
||||
|
||||
NSData *dataToParse = nil;
|
||||
if (signature == 0x3072cfa1) {
|
||||
NSData *bytes = [inputStream readBytes];
|
||||
if (bytes != nil) {
|
||||
dataToParse = [MTGzip decompress:bytes];
|
||||
}
|
||||
} else {
|
||||
dataToParse = data;
|
||||
}
|
||||
|
||||
if (dataToParse == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [Api1__Environment parseObject:dataToParse];
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "StoredAccountInfos.h"
|
||||
#import "Api.h"
|
||||
#import <BuildConfig/BuildConfig.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
dispatch_block_t fetchImage(BuildConfig *buildConfig, AccountProxyConnection * _Nullable proxyConnection, StoredAccountInfo *account, Api1_InputFileLocation *inputFileLocation, int32_t datacenterId, void (^_completion)(NSData * _Nullable));
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -1,209 +0,0 @@
|
||||
#import "FetchImage.h"
|
||||
|
||||
#import <MtProtoKit/MtProtoKit.h>
|
||||
#import <OpenSSLEncryptionProvider/OpenSSLEncryptionProvider.h>
|
||||
|
||||
#import "Serialization.h"
|
||||
|
||||
@interface InMemoryKeychain : NSObject <MTKeychain> {
|
||||
NSMutableDictionary *_dict;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation InMemoryKeychain
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_dict = [[NSMutableDictionary alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setObject:(id)object forKey:(NSString *)aKey group:(NSString *)group {
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:object];
|
||||
_dict[[NSString stringWithFormat:@"%@:%@", group, aKey]] = data;
|
||||
}
|
||||
|
||||
- (id)objectForKey:(NSString *)aKey group:(NSString *)group {
|
||||
NSData *data = _dict[[NSString stringWithFormat:@"%@:%@", group, aKey]];
|
||||
if (data != nil) {
|
||||
return [NSKeyedUnarchiver unarchiveObjectWithData:data];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeObjectForKey:(NSString *)aKey group:(NSString *)group {
|
||||
[_dict removeObjectForKey:[NSString stringWithFormat:@"%@:%@", group, aKey]];
|
||||
}
|
||||
|
||||
- (void)dropGroup:(NSString *)group {
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static void MTLoggingFunction(NSString *string, va_list args) {
|
||||
NSLogv(string, args);
|
||||
}
|
||||
|
||||
@interface ParsedFile : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData * _Nullable data;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ParsedFile
|
||||
|
||||
- (instancetype)initWithData:(NSData * _Nullable)data {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_data = data;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
dispatch_block_t fetchImage(BuildConfig *buildConfig, AccountProxyConnection * _Nullable proxyConnection, StoredAccountInfo *account, Api1_InputFileLocation *inputFileLocation, int32_t datacenterId, void (^completion)(NSData * _Nullable)) {
|
||||
MTLogSetEnabled(true);
|
||||
MTLogSetLoggingFunction(&MTLoggingFunction);
|
||||
|
||||
Serialization *serialization = [[Serialization alloc] init];
|
||||
|
||||
MTApiEnvironment *apiEnvironment = [[MTApiEnvironment alloc] init];
|
||||
|
||||
apiEnvironment.apiId = buildConfig.apiId;
|
||||
apiEnvironment.langPack = @"ios";
|
||||
apiEnvironment.layer = @([serialization currentLayer]);
|
||||
apiEnvironment.disableUpdates = true;
|
||||
apiEnvironment = [apiEnvironment withUpdatedLangPackCode:@""];
|
||||
|
||||
if (proxyConnection != nil) {
|
||||
apiEnvironment = [apiEnvironment withUpdatedSocksProxySettings:[[MTSocksProxySettings alloc] initWithIp:proxyConnection.host port:(uint16_t)proxyConnection.port username:proxyConnection.username password:proxyConnection.password secret:proxyConnection.secret]];
|
||||
}
|
||||
|
||||
MTContext *context = [[MTContext alloc] initWithSerialization:serialization encryptionProvider:[[OpenSSLEncryptionProvider alloc] init] apiEnvironment:apiEnvironment isTestingEnvironment:account.isTestingEnvironment useTempAuthKeys:true];
|
||||
context.tempKeyExpiration = 10 * 60 * 60;
|
||||
|
||||
NSDictionary *seedAddressList = @{};
|
||||
|
||||
if (account.isTestingEnvironment) {
|
||||
seedAddressList = @{
|
||||
@(1): @[@"149.154.175.10"],
|
||||
@(2): @[@"149.154.167.40"]
|
||||
};
|
||||
} else {
|
||||
seedAddressList = @{
|
||||
@(1): @[@"149.154.175.50", @"2001:b28:f23d:f001::a"],
|
||||
@(2): @[@"149.154.167.50", @"2001:67c:4e8:f002::a"],
|
||||
@(3): @[@"149.154.175.100", @"2001:b28:f23d:f003::a"],
|
||||
@(4): @[@"149.154.167.91", @"2001:67c:4e8:f004::a"],
|
||||
@(5): @[@"149.154.171.5", @"2001:b28:f23f:f005::a"]
|
||||
};
|
||||
}
|
||||
|
||||
for (NSNumber *datacenterId in seedAddressList) {
|
||||
NSMutableArray *addressList = [[NSMutableArray alloc] init];
|
||||
for (NSString *host in seedAddressList[datacenterId]) {
|
||||
[addressList addObject:[[MTDatacenterAddress alloc] initWithIp:host port:443 preferForMedia:false restrictToTcp:false cdn:false preferForProxy:false secret:nil]];
|
||||
}
|
||||
[context setSeedAddressSetForDatacenterWithId:[datacenterId intValue] seedAddressSet:[[MTDatacenterAddressSet alloc] initWithAddressList:addressList]];
|
||||
}
|
||||
|
||||
InMemoryKeychain *keychain = [[InMemoryKeychain alloc] init];
|
||||
context.keychain = keychain;
|
||||
|
||||
[context performBatchUpdates:^{
|
||||
for (NSNumber *datacenterId in account.datacenters) {
|
||||
AccountDatacenterInfo *info = account.datacenters[datacenterId];
|
||||
if (info.addressList.count != 0) {
|
||||
NSMutableArray *list = [[NSMutableArray alloc] init];
|
||||
for (AccountDatacenterAddress *address in info.addressList) {
|
||||
[list addObject:[[MTDatacenterAddress alloc] initWithIp:address.host port:address.port preferForMedia:address.isMedia restrictToTcp:false cdn:false preferForProxy:address.isProxy secret:address.secret]];
|
||||
}
|
||||
[context updateAddressSetForDatacenterWithId:[datacenterId intValue] addressSet:[[MTDatacenterAddressSet alloc] initWithAddressList:list] forceUpdateSchemes:true];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
for (NSNumber *datacenterId in account.datacenters) {
|
||||
AccountDatacenterInfo *info = account.datacenters[datacenterId];
|
||||
MTDatacenterAuthInfo *authInfo = [[MTDatacenterAuthInfo alloc] initWithAuthKey:info.masterKey.data authKeyId:info.masterKey.keyId saltSet:@[] authKeyAttributes:@{}];
|
||||
|
||||
[context updateAuthInfoForDatacenterWithId:[datacenterId intValue] authInfo:authInfo selector:MTDatacenterAuthInfoSelectorPersistent];
|
||||
|
||||
if (info.ephemeralMainKey != nil) {
|
||||
MTDatacenterAuthInfo *ephemeralMainAuthInfo = [[MTDatacenterAuthInfo alloc] initWithAuthKey:info.ephemeralMainKey.data authKeyId:info.ephemeralMainKey.keyId saltSet:@[] authKeyAttributes:@{}];
|
||||
[context updateAuthInfoForDatacenterWithId:[datacenterId intValue] authInfo:ephemeralMainAuthInfo selector:MTDatacenterAuthInfoSelectorEphemeralMain];
|
||||
}
|
||||
|
||||
if (info.ephemeralMediaKey != nil) {
|
||||
MTDatacenterAuthInfo *ephemeralMediaAuthInfo = [[MTDatacenterAuthInfo alloc] initWithAuthKey:info.ephemeralMediaKey.data authKeyId:info.ephemeralMediaKey.keyId saltSet:@[] authKeyAttributes:@{}];
|
||||
[context updateAuthInfoForDatacenterWithId:[datacenterId intValue] authInfo:ephemeralMediaAuthInfo selector:MTDatacenterAuthInfoSelectorEphemeralMedia];
|
||||
}
|
||||
}
|
||||
|
||||
MTProto *mtProto = [[MTProto alloc] initWithContext:context datacenterId:datacenterId usageCalculationInfo:nil requiredAuthToken:nil authTokenMasterDatacenterId:0];
|
||||
mtProto.useTempAuthKeys = context.useTempAuthKeys;
|
||||
mtProto.checkForProxyConnectionIssues = false;
|
||||
|
||||
MTRequestMessageService *requestService = [[MTRequestMessageService alloc] initWithContext:context];
|
||||
[mtProto addMessageService:requestService];
|
||||
|
||||
MTRequest *request = [[MTRequest alloc] init];
|
||||
|
||||
MTOutputStream *outputStream = [[MTOutputStream alloc] init];
|
||||
[outputStream writeInt32:-475607115]; //upload.getFile
|
||||
[outputStream writeData:[Api1__Environment serializeObject:inputFileLocation]];
|
||||
|
||||
[outputStream writeInt32:0];
|
||||
[outputStream writeInt32:32 * 1024];
|
||||
|
||||
[request setPayload:[outputStream currentBytes] metadata:@"getFile" shortMetadata:@"getFile" responseParser:^id(NSData *response) {
|
||||
MTInputStream *inputStream = [[MTInputStream alloc] initWithData:response];
|
||||
int32_t signature = [inputStream readInt32];
|
||||
if (signature != 157948117) {
|
||||
return [[ParsedFile alloc] initWithData:nil];
|
||||
}
|
||||
[inputStream readInt32]; //type
|
||||
[inputStream readInt32]; //mtime
|
||||
|
||||
return [[ParsedFile alloc] initWithData:[inputStream readBytes]];
|
||||
}];
|
||||
|
||||
request.dependsOnPasswordEntry = false;
|
||||
request.shouldContinueExecutionWithErrorContext = ^bool (__unused MTRequestErrorContext *errorContext) {
|
||||
return true;
|
||||
};
|
||||
|
||||
request.completed = ^(id boxedResponse, __unused NSTimeInterval completionTimestamp, MTRpcError *error) {
|
||||
if (error != nil) {
|
||||
if (completion) {
|
||||
completion(nil);
|
||||
}
|
||||
} else {
|
||||
if ([boxedResponse isKindOfClass:[ParsedFile class]]) {
|
||||
if (completion) {
|
||||
completion(((ParsedFile *)boxedResponse).data);
|
||||
}
|
||||
} else {
|
||||
if (completion) {
|
||||
completion(nil);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
[requestService addRequest:request];
|
||||
[mtProto resume];
|
||||
|
||||
id internalId = request.internalId;
|
||||
return ^{
|
||||
[requestService removeRequestByInternalId:internalId];
|
||||
[context performBatchUpdates:^{
|
||||
}];
|
||||
[mtProto stop];
|
||||
};
|
||||
}
|
||||
@ -1,494 +0,0 @@
|
||||
#import <NotificationServiceObjC/NotificationServiceObjC.h>
|
||||
|
||||
#import <mach/mach.h>
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <BuildConfig/BuildConfig.h>
|
||||
|
||||
#ifdef __IPHONE_13_0
|
||||
#import <BackgroundTasks/BackgroundTasks.h>
|
||||
#endif
|
||||
|
||||
#import "StoredAccountInfos.h"
|
||||
#import "Attachments.h"
|
||||
#import "Api.h"
|
||||
#import "FetchImage.h"
|
||||
|
||||
static NSData * _Nullable parseBase64(NSString *string) {
|
||||
string = [string stringByReplacingOccurrencesOfString:@"-" withString:@"+"];
|
||||
string = [string stringByReplacingOccurrencesOfString:@"_" withString:@"/"];
|
||||
while (string.length % 4 != 0) {
|
||||
string = [string stringByAppendingString:@"="];
|
||||
}
|
||||
return [[NSData alloc] initWithBase64EncodedString:string options:0];
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
PeerNamespaceCloudUser = 0,
|
||||
PeerNamespaceCloudGroup = 1,
|
||||
PeerNamespaceCloudChannel = 2,
|
||||
PeerNamespaceSecretChat = 3
|
||||
} PeerNamespace;
|
||||
|
||||
static int64_t makePeerId(int32_t namespace, int32_t value) {
|
||||
return (((int64_t)(namespace)) << 32) | ((int64_t)((uint64_t)((uint32_t)value)));
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
static void reportMemory() {
|
||||
struct task_basic_info info;
|
||||
mach_msg_type_number_t size = TASK_BASIC_INFO_COUNT;
|
||||
kern_return_t kerr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
|
||||
if (kerr == KERN_SUCCESS) {
|
||||
NSLog(@"Memory in use (in bytes): %lu", info.resident_size);
|
||||
NSLog(@"Memory in use (in MiB): %f", ((CGFloat)info.resident_size / 1048576));
|
||||
} else {
|
||||
NSLog(@"Error with task_info(): %s", mach_error_string(kerr));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@interface NotificationServiceImpl () {
|
||||
void (^_serialDispatch)(dispatch_block_t);
|
||||
void (^_countIncomingMessage)(NSString *, int64_t, DeviceSpecificEncryptionParameters *, int64_t, int32_t);
|
||||
|
||||
NSString * _Nullable _rootPath;
|
||||
DeviceSpecificEncryptionParameters * _Nullable _deviceSpecificEncryptionParameters;
|
||||
bool _isLockedValue;
|
||||
NSString *_lockedMessageTextValue;
|
||||
NSString * _Nullable _baseAppBundleId;
|
||||
void (^_contentHandler)(UNNotificationContent *);
|
||||
UNMutableNotificationContent * _Nullable _bestAttemptContent;
|
||||
void (^_cancelFetch)(void);
|
||||
|
||||
NSNumber * _Nullable _updatedUnreadCount;
|
||||
bool _contentReady;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NotificationServiceImpl
|
||||
|
||||
- (instancetype)initWithSerialDispatch:(void (^)(dispatch_block_t))serialDispatch countIncomingMessage:(void (^)(NSString *, int64_t, DeviceSpecificEncryptionParameters *, int64_t, int32_t))countIncomingMessage isLocked:(nonnull bool (^)(NSString * _Nonnull))isLocked lockedMessageText:(NSString *(^)(NSString *))lockedMessageText {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
#if DEBUG
|
||||
reportMemory();
|
||||
#endif
|
||||
|
||||
_serialDispatch = [serialDispatch copy];
|
||||
_countIncomingMessage = [countIncomingMessage copy];
|
||||
|
||||
NSString *appBundleIdentifier = [NSBundle mainBundle].bundleIdentifier;
|
||||
NSRange lastDotRange = [appBundleIdentifier rangeOfString:@"." options:NSBackwardsSearch];
|
||||
if (lastDotRange.location != NSNotFound) {
|
||||
_baseAppBundleId = [appBundleIdentifier substringToIndex:lastDotRange.location];
|
||||
NSString *appGroupName = [@"group." stringByAppendingString:_baseAppBundleId];
|
||||
NSURL *appGroupUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupName];
|
||||
|
||||
if (appGroupUrl != nil) {
|
||||
NSString *rootPath = [[appGroupUrl path] stringByAppendingPathComponent:@"telegram-data"];
|
||||
_rootPath = rootPath;
|
||||
if (rootPath != nil) {
|
||||
_deviceSpecificEncryptionParameters = [BuildConfig deviceSpecificEncryptionParameters:rootPath baseAppBundleId:_baseAppBundleId];
|
||||
|
||||
_isLockedValue = isLocked(rootPath);
|
||||
if (_isLockedValue) {
|
||||
_lockedMessageTextValue = lockedMessageText(rootPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NSAssert(false, @"appGroupUrl == nil");
|
||||
}
|
||||
} else {
|
||||
NSAssert(false, @"Invalid bundle id");
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)completeWithBestAttemptContent {
|
||||
_contentReady = true;
|
||||
_updatedUnreadCount = @(-1);
|
||||
if (_contentReady && _updatedUnreadCount) {
|
||||
[self _internalComplete];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateUnreadCount:(int32_t)unreadCount {
|
||||
_updatedUnreadCount = @(unreadCount);
|
||||
if (_contentReady && _updatedUnreadCount) {
|
||||
[self _internalComplete];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_internalComplete {
|
||||
#if DEBUG
|
||||
reportMemory();
|
||||
#endif
|
||||
|
||||
NSString *baseAppBundleId = _baseAppBundleId;
|
||||
void (^contentHandler)(UNNotificationContent *) = [_contentHandler copy];
|
||||
UNMutableNotificationContent *bestAttemptContent = _bestAttemptContent;
|
||||
NSNumber *updatedUnreadCount = updatedUnreadCount;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
#ifdef __IPHONE_13_0
|
||||
if (baseAppBundleId != nil && false) {
|
||||
BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:[baseAppBundleId stringByAppendingString:@".refresh"]];
|
||||
request.earliestBeginDate = nil;
|
||||
NSError *error = nil;
|
||||
[[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error];
|
||||
if (error != nil) {
|
||||
NSLog(@"Error: %@", error);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (updatedUnreadCount != nil) {
|
||||
int32_t unreadCount = (int32_t)[updatedUnreadCount intValue];
|
||||
if (unreadCount > 0) {
|
||||
bestAttemptContent.badge = @(unreadCount);
|
||||
}
|
||||
}
|
||||
contentHandler(bestAttemptContent);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
|
||||
if (_rootPath == nil) {
|
||||
_bestAttemptContent = (UNMutableNotificationContent *)[request.content mutableCopy];
|
||||
[self completeWithBestAttemptContent];
|
||||
return;
|
||||
}
|
||||
|
||||
_contentHandler = [contentHandler copy];
|
||||
_bestAttemptContent = (UNMutableNotificationContent *)[request.content mutableCopy];
|
||||
|
||||
NSString * _Nullable encryptedPayload = request.content.userInfo[@"p"];
|
||||
NSData * _Nullable encryptedData = nil;
|
||||
if (encryptedPayload != nil && [encryptedPayload isKindOfClass:[NSString class]]) {
|
||||
encryptedData = parseBase64(encryptedPayload);
|
||||
}
|
||||
|
||||
StoredAccountInfos * _Nullable accountInfos = [StoredAccountInfos loadFromPath:[_rootPath stringByAppendingPathComponent:@"accounts-shared-data"]];
|
||||
|
||||
int selectedAccountIndex = -1;
|
||||
NSDictionary *decryptedPayload = decryptedNotificationPayload(accountInfos.accounts, encryptedData, &selectedAccountIndex);
|
||||
|
||||
if (decryptedPayload != nil && selectedAccountIndex != -1) {
|
||||
StoredAccountInfo *account = accountInfos.accounts[selectedAccountIndex];
|
||||
|
||||
NSMutableDictionary *userInfo = nil;
|
||||
if (_bestAttemptContent.userInfo != nil) {
|
||||
userInfo = [[NSMutableDictionary alloc] initWithDictionary:_bestAttemptContent.userInfo];
|
||||
} else {
|
||||
userInfo = [[NSMutableDictionary alloc] init];
|
||||
}
|
||||
userInfo[@"accountId"] = @(account.accountId);
|
||||
|
||||
int64_t peerId = 0;
|
||||
int32_t messageId = 0;
|
||||
bool silent = false;
|
||||
|
||||
NSString *messageIdString = decryptedPayload[@"msg_id"];
|
||||
if ([messageIdString isKindOfClass:[NSString class]]) {
|
||||
userInfo[@"msg_id"] = messageIdString;
|
||||
messageId = [messageIdString intValue];
|
||||
}
|
||||
|
||||
NSString *fromIdString = decryptedPayload[@"from_id"];
|
||||
if ([fromIdString isKindOfClass:[NSString class]]) {
|
||||
userInfo[@"from_id"] = fromIdString;
|
||||
peerId = makePeerId(PeerNamespaceCloudUser, [fromIdString intValue]);
|
||||
}
|
||||
|
||||
NSString *chatIdString = decryptedPayload[@"chat_id"];
|
||||
if ([chatIdString isKindOfClass:[NSString class]]) {
|
||||
userInfo[@"chat_id"] = chatIdString;
|
||||
peerId = makePeerId(PeerNamespaceCloudGroup, [chatIdString intValue]);
|
||||
}
|
||||
|
||||
NSString *channelIdString = decryptedPayload[@"channel_id"];
|
||||
if ([channelIdString isKindOfClass:[NSString class]]) {
|
||||
userInfo[@"channel_id"] = channelIdString;
|
||||
peerId = makePeerId(PeerNamespaceCloudChannel, [channelIdString intValue]);
|
||||
}
|
||||
|
||||
/*if (_countIncomingMessage && _deviceSpecificEncryptionParameters) {
|
||||
_countIncomingMessage(_rootPath, account.accountId, _deviceSpecificEncryptionParameters, peerId, messageId);
|
||||
}*/
|
||||
|
||||
NSString *silentString = decryptedPayload[@"silent"];
|
||||
if ([silentString isKindOfClass:[NSString class]]) {
|
||||
silent = [silentString intValue] != 0;
|
||||
}
|
||||
|
||||
NSData *attachmentData = nil;
|
||||
id parsedAttachment = nil;
|
||||
|
||||
if (!_isLockedValue) {
|
||||
NSString *attachmentDataString = decryptedPayload[@"attachb64"];
|
||||
if ([attachmentDataString isKindOfClass:[NSString class]]) {
|
||||
attachmentData = parseBase64(attachmentDataString);
|
||||
if (attachmentData != nil) {
|
||||
parsedAttachment = parseAttachment(attachmentData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSString *imagesPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"aps-data"];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:imagesPath withIntermediateDirectories:true attributes:nil error:nil];
|
||||
NSString *accountBasePath = [_rootPath stringByAppendingPathComponent:[NSString stringWithFormat:@"account-%llud", account.accountId]];
|
||||
|
||||
NSString *mediaBoxPath = [accountBasePath stringByAppendingPathComponent:@"/postbox/media"];
|
||||
|
||||
NSString *tempImagePath = nil;
|
||||
NSString *mediaBoxThumbnailImagePath = nil;
|
||||
|
||||
int32_t fileDatacenterId = 0;
|
||||
Api1_InputFileLocation *inputFileLocation = nil;
|
||||
int32_t progressiveFileLimit = -1;
|
||||
|
||||
NSString *fetchResourceId = nil;
|
||||
bool isPng = false;
|
||||
bool isExpandableMedia = false;
|
||||
|
||||
if (parsedAttachment != nil) {
|
||||
if ([parsedAttachment isKindOfClass:[Api1_Photo_photo class]]) {
|
||||
Api1_Photo_photo *photo = parsedAttachment;
|
||||
isExpandableMedia = true;
|
||||
|
||||
/*for (id size in photo.sizes) {
|
||||
if ([size isKindOfClass:[Api1_PhotoSize_photoSizeProgressive class]]) {
|
||||
Api1_PhotoSize_photoSizeProgressive *sizeValue = size;
|
||||
inputFileLocation = [Api1_InputFileLocation inputPhotoFileLocationWithPid:photo.pid accessHash:photo.accessHash fileReference:photo.fileReference thumbSize:sizeValue.type];
|
||||
fileDatacenterId = [photo.dcId intValue];
|
||||
fetchResourceId = [NSString stringWithFormat:@"telegram-cloud-photo-size-%@-%@-%@", photo.dcId, photo.pid, sizeValue.type];
|
||||
break;
|
||||
}
|
||||
}*/
|
||||
|
||||
if (inputFileLocation == nil) {
|
||||
for (id size in photo.sizes) {
|
||||
if ([size isKindOfClass:[Api1_PhotoSize_photoSize class]]) {
|
||||
Api1_PhotoSize_photoSize *sizeValue = size;
|
||||
if ([sizeValue.type isEqualToString:@"m"]) {
|
||||
inputFileLocation = [Api1_InputFileLocation inputPhotoFileLocationWithPid:photo.pid accessHash:photo.accessHash fileReference:photo.fileReference thumbSize:sizeValue.type];
|
||||
fileDatacenterId = [photo.dcId intValue];
|
||||
fetchResourceId = [NSString stringWithFormat:@"telegram-cloud-photo-size-%@-%@-%@", photo.dcId, photo.pid, sizeValue.type];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ([parsedAttachment isKindOfClass:[Api1_Document_document class]]) {
|
||||
Api1_Document_document *document = parsedAttachment;
|
||||
|
||||
bool isSticker = false;
|
||||
for (id attribute in document.attributes) {
|
||||
if ([attribute isKindOfClass:[Api1_DocumentAttribute_documentAttributeSticker class]]) {
|
||||
isSticker = true;
|
||||
}
|
||||
}
|
||||
bool isAnimatedSticker = [document.mimeType isEqualToString:@"application/x-tgsticker"];
|
||||
if (isSticker || isAnimatedSticker) {
|
||||
isExpandableMedia = true;
|
||||
}
|
||||
for (id size in document.thumbs) {
|
||||
if ([size isKindOfClass:[Api1_PhotoSize_photoSize class]]) {
|
||||
Api1_PhotoSize_photoSize *photoSize = size;
|
||||
if ((isSticker && [photoSize.type isEqualToString:@"s"]) || [photoSize.type isEqualToString:@"m"]) {
|
||||
if (isSticker) {
|
||||
isPng = true;
|
||||
}
|
||||
inputFileLocation = [Api1_InputFileLocation inputDocumentFileLocationWithPid:document.pid accessHash:document.accessHash fileReference:document.fileReference thumbSize:photoSize.type];
|
||||
fileDatacenterId = [document.dcId intValue];
|
||||
fetchResourceId = [NSString stringWithFormat:@"telegram-cloud-document-size-%@-%@-%@", document.dcId, document.pid, photoSize.type];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fetchResourceId != nil) {
|
||||
tempImagePath = [imagesPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", fetchResourceId, isPng ? @"png" : @"jpg"]];
|
||||
mediaBoxThumbnailImagePath = [mediaBoxPath stringByAppendingPathComponent:fetchResourceId];
|
||||
}
|
||||
|
||||
NSDictionary *aps = decryptedPayload[@"aps"];
|
||||
if ([aps isKindOfClass:[NSDictionary class]]) {
|
||||
id alert = aps[@"alert"];
|
||||
if ([alert isKindOfClass:[NSDictionary class]]) {
|
||||
NSDictionary *alertDict = alert;
|
||||
NSString *title = alertDict[@"title"];
|
||||
NSString *subtitle = alertDict[@"subtitle"];
|
||||
NSString *body = alertDict[@"body"];
|
||||
if (![title isKindOfClass:[NSString class]]) {
|
||||
title = @"";
|
||||
}
|
||||
if (![subtitle isKindOfClass:[NSString class]]) {
|
||||
subtitle = @"";
|
||||
}
|
||||
if (![body isKindOfClass:[NSString class]]) {
|
||||
body = nil;
|
||||
}
|
||||
if (title.length != 0 && silent) {
|
||||
title = [title stringByAppendingString:@" 🔕"];
|
||||
}
|
||||
_bestAttemptContent.title = title;
|
||||
if (_isLockedValue) {
|
||||
_bestAttemptContent.title = @"";
|
||||
_bestAttemptContent.subtitle = @"";
|
||||
if (_lockedMessageTextValue != nil) {
|
||||
_bestAttemptContent.body = _lockedMessageTextValue;
|
||||
} else {
|
||||
_bestAttemptContent.body = @"^You have a new message";
|
||||
}
|
||||
} else {
|
||||
_bestAttemptContent.subtitle = subtitle;
|
||||
_bestAttemptContent.body = body;
|
||||
}
|
||||
} else if ([alert isKindOfClass:[NSString class]]) {
|
||||
_bestAttemptContent.title = @"";
|
||||
_bestAttemptContent.subtitle = @"";
|
||||
if (_isLockedValue) {
|
||||
if (_lockedMessageTextValue != nil) {
|
||||
_bestAttemptContent.body = _lockedMessageTextValue;
|
||||
} else {
|
||||
_bestAttemptContent.body = @"^You have a new message";
|
||||
}
|
||||
} else {
|
||||
_bestAttemptContent.body = alert;
|
||||
}
|
||||
}
|
||||
|
||||
if (_isLockedValue) {
|
||||
_bestAttemptContent.threadIdentifier = @"locked";
|
||||
} else {
|
||||
NSString *threadIdString = aps[@"thread-id"];
|
||||
if ([threadIdString isKindOfClass:[NSString class]]) {
|
||||
_bestAttemptContent.threadIdentifier = threadIdString;
|
||||
}
|
||||
}
|
||||
NSString *soundString = aps[@"sound"];
|
||||
if ([soundString isKindOfClass:[NSString class]]) {
|
||||
_bestAttemptContent.sound = [UNNotificationSound soundNamed:soundString];
|
||||
}
|
||||
if (_isLockedValue) {
|
||||
_bestAttemptContent.categoryIdentifier = @"locked";
|
||||
} else {
|
||||
NSString *categoryString = aps[@"category"];
|
||||
if ([categoryString isKindOfClass:[NSString class]]) {
|
||||
_bestAttemptContent.categoryIdentifier = categoryString;
|
||||
if (peerId != 0 && messageId != 0 && parsedAttachment != nil && attachmentData != nil) {
|
||||
userInfo[@"peerId"] = @(peerId);
|
||||
userInfo[@"messageId.namespace"] = @(0);
|
||||
userInfo[@"messageId.id"] = @(messageId);
|
||||
|
||||
userInfo[@"media"] = [attachmentData base64EncodedStringWithOptions:0];
|
||||
|
||||
if (isExpandableMedia) {
|
||||
if ([categoryString isEqualToString:@"r"]) {
|
||||
_bestAttemptContent.categoryIdentifier = @"withReplyMedia";
|
||||
} else if ([categoryString isEqualToString:@"m"]) {
|
||||
_bestAttemptContent.categoryIdentifier = @"withMuteMedia";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (accountInfos.accounts.count > 1) {
|
||||
if (_bestAttemptContent.title.length != 0 && account.peerName.length != 0) {
|
||||
_bestAttemptContent.title = [NSString stringWithFormat:@"%@ → %@", _bestAttemptContent.title, account.peerName];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_bestAttemptContent.userInfo = userInfo;
|
||||
|
||||
if (_cancelFetch) {
|
||||
_cancelFetch();
|
||||
_cancelFetch = nil;
|
||||
}
|
||||
|
||||
if (mediaBoxThumbnailImagePath != nil && tempImagePath != nil && inputFileLocation != nil) {
|
||||
NSData *data = [NSData dataWithContentsOfFile:mediaBoxThumbnailImagePath];
|
||||
if (data != nil) {
|
||||
NSData *tempData = data;
|
||||
if (isPng) {
|
||||
/*if let image = WebP.convert(fromWebP: data), let imageData = image.pngData() {
|
||||
tempData = imageData
|
||||
}*/
|
||||
}
|
||||
if ([tempData writeToFile:tempImagePath atomically:true]) {
|
||||
UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"image" URL:[NSURL fileURLWithPath:tempImagePath] options:nil error:nil];
|
||||
if (attachment != nil) {
|
||||
_bestAttemptContent.attachments = @[attachment];
|
||||
}
|
||||
}
|
||||
[self completeWithBestAttemptContent];
|
||||
} else {
|
||||
BuildConfig *buildConfig = [[BuildConfig alloc] initWithBaseAppBundleId:_baseAppBundleId];
|
||||
|
||||
void (^serialDispatch)(dispatch_block_t) = _serialDispatch;
|
||||
|
||||
__weak typeof(self) weakSelf = self;
|
||||
_cancelFetch = fetchImage(buildConfig, accountInfos.proxy, account, inputFileLocation, fileDatacenterId, ^(NSData * _Nullable data) {
|
||||
serialDispatch(^{
|
||||
__strong typeof(weakSelf) strongSelf = weakSelf;
|
||||
if (strongSelf == nil) {
|
||||
return;
|
||||
}
|
||||
if (strongSelf->_cancelFetch) {
|
||||
strongSelf->_cancelFetch();
|
||||
strongSelf->_cancelFetch = nil;
|
||||
|
||||
if (data != nil) {
|
||||
[data writeToFile:mediaBoxThumbnailImagePath atomically:true];
|
||||
NSData *tempData = data;
|
||||
if (isPng) {
|
||||
/*if let image = WebP.convert(fromWebP: data), let imageData = image.pngData() {
|
||||
tempData = imageData
|
||||
}*/
|
||||
}
|
||||
if ([tempData writeToFile:tempImagePath atomically:true]) {
|
||||
UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"image" URL:[NSURL fileURLWithPath:tempImagePath] options:nil error:nil];
|
||||
if (attachment != nil) {
|
||||
strongSelf->_bestAttemptContent.attachments = @[attachment];
|
||||
}
|
||||
}
|
||||
}
|
||||
[strongSelf completeWithBestAttemptContent];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
[self completeWithBestAttemptContent];
|
||||
}
|
||||
} else {
|
||||
[self completeWithBestAttemptContent];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)serviceExtensionTimeWillExpire {
|
||||
if (_cancelFetch) {
|
||||
_cancelFetch();
|
||||
_cancelFetch = nil;
|
||||
}
|
||||
|
||||
if (_contentHandler) {
|
||||
if(_bestAttemptContent) {
|
||||
if (_updatedUnreadCount == nil) {
|
||||
_updatedUnreadCount = @(-1);
|
||||
}
|
||||
[self completeWithBestAttemptContent];
|
||||
}
|
||||
_contentHandler = nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@ -1,11 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <MTProtoKit/MTProtoKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface Serialization : NSObject <MTSerialization>
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -1,35 +0,0 @@
|
||||
#import "Serialization.h"
|
||||
|
||||
@implementation Serialization
|
||||
|
||||
- (NSUInteger)currentLayer {
|
||||
return 133;
|
||||
}
|
||||
|
||||
- (id _Nullable)parseMessage:(NSData * _Nullable)data {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (MTExportAuthorizationResponseParser _Nonnull)exportAuthorization:(int32_t)datacenterId data:(__autoreleasing NSData **)data {
|
||||
return ^MTExportedAuthorizationData *(NSData *resultData) {
|
||||
return nil;
|
||||
};
|
||||
}
|
||||
|
||||
- (NSData * _Nonnull)importAuthorization:(int64_t)authId bytes:(NSData * _Nonnull)bytes {
|
||||
return [NSData data];
|
||||
}
|
||||
|
||||
- (MTRequestDatacenterAddressListParser)requestDatacenterAddressWithData:(__autoreleasing NSData **)data {
|
||||
return ^MTDatacenterAddressListData *(NSData *resultData) {
|
||||
return nil;
|
||||
};
|
||||
}
|
||||
|
||||
- (MTRequestNoopParser)requestNoop:(__autoreleasing NSData **)data {
|
||||
return ^id(NSData *resultData) {
|
||||
return nil;
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
@ -1,69 +0,0 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AccountNotificationKey: NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSData *keyId;
|
||||
@property (nonatomic, strong, readonly) NSData *data;
|
||||
|
||||
@end
|
||||
|
||||
@interface AccountDatacenterKey: NSObject
|
||||
|
||||
@property (nonatomic, readonly) int64_t keyId;
|
||||
@property (nonatomic, strong, readonly) NSData *data;
|
||||
|
||||
@end
|
||||
|
||||
@interface AccountDatacenterAddress: NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString *host;
|
||||
@property (nonatomic, readonly) int32_t port;
|
||||
@property (nonatomic, readonly) bool isMedia;
|
||||
@property (nonatomic, strong, readonly) NSData * _Nullable secret;
|
||||
|
||||
@end
|
||||
|
||||
@interface AccountDatacenterInfo: NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) AccountDatacenterKey *masterKey;
|
||||
@property (nonatomic, strong, readonly) AccountDatacenterKey *ephemeralMainKey;
|
||||
@property (nonatomic, strong, readonly) AccountDatacenterKey *ephemeralMediaKey;
|
||||
@property (nonatomic, strong, readonly) NSArray<AccountDatacenterAddress *> *addressList;
|
||||
|
||||
@end
|
||||
|
||||
@interface AccountProxyConnection: NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString *host;
|
||||
@property (nonatomic, readonly) int32_t port;
|
||||
@property (nonatomic, strong) NSString * _Nullable username;
|
||||
@property (nonatomic, strong) NSString * _Nullable password;
|
||||
@property (nonatomic, strong) NSData * _Nullable secret;
|
||||
|
||||
@end
|
||||
|
||||
@interface StoredAccountInfo : NSObject
|
||||
|
||||
@property (nonatomic, readonly) int64_t accountId;
|
||||
@property (nonatomic, readonly) int32_t primaryId;
|
||||
@property (nonatomic, readonly) bool isTestingEnvironment;
|
||||
@property (nonatomic, strong, readonly) NSString *peerName;
|
||||
@property (nonatomic, strong, readonly) NSDictionary<NSNumber *, AccountDatacenterInfo *> *datacenters;
|
||||
@property (nonatomic, strong, readonly) AccountNotificationKey *notificationKey;
|
||||
|
||||
@end
|
||||
|
||||
@interface StoredAccountInfos : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) AccountProxyConnection * _Nullable proxy;
|
||||
@property (nonatomic, strong, readonly) NSArray<StoredAccountInfo *> *accounts;
|
||||
|
||||
+ (StoredAccountInfos * _Nullable)loadFromPath:(NSString *)path;
|
||||
|
||||
@end
|
||||
|
||||
NSDictionary * _Nullable decryptedNotificationPayload(NSArray<StoredAccountInfo *> *accounts, NSData *data, int *selectedAccountIndex);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -1,427 +0,0 @@
|
||||
#import "StoredAccountInfos.h"
|
||||
|
||||
#import <MTProtoKit/MTProtoKit.h>
|
||||
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
|
||||
@implementation AccountNotificationKey
|
||||
|
||||
- (instancetype)initWithKeyId:(NSData *)keyId data:(NSData *)data {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_keyId = keyId;
|
||||
_data = data;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype _Nullable)parse:(NSDictionary *)dict {
|
||||
NSString *keyIdString = dict[@"id"];
|
||||
NSData *keyId = nil;
|
||||
if ([keyIdString isKindOfClass:[NSString class]]) {
|
||||
keyId = [[NSData alloc] initWithBase64EncodedString:keyIdString options:0];
|
||||
}
|
||||
if (keyId == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *dataString = dict[@"data"];
|
||||
NSData *data = nil;
|
||||
if ([dataString isKindOfClass:[NSString class]]) {
|
||||
data = [[NSData alloc] initWithBase64EncodedString:dataString options:0];
|
||||
}
|
||||
if (data == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [[AccountNotificationKey alloc] initWithKeyId:keyId data:data];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation AccountDatacenterKey
|
||||
|
||||
- (instancetype)initWithKeyId:(int64_t)keyId data:(NSData *)data {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_keyId = keyId;
|
||||
_data = data;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype _Nullable)parse:(NSDictionary *)dict {
|
||||
NSNumber *keyIdNumber = dict[@"id"];
|
||||
if (![keyIdNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
int64_t keyId = [keyIdNumber longLongValue];
|
||||
|
||||
NSString *dataString = dict[@"data"];
|
||||
NSData *data = nil;
|
||||
if ([dataString isKindOfClass:[NSString class]]) {
|
||||
data = [[NSData alloc] initWithBase64EncodedString:dataString options:0];
|
||||
}
|
||||
if (data == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [[AccountDatacenterKey alloc] initWithKeyId:keyId data:data];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation AccountDatacenterAddress
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host port:(int32_t)port isMedia:(bool)isMedia secret:(NSData * _Nullable)secret {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_host = host;
|
||||
_port = port;
|
||||
_isMedia = isMedia;
|
||||
_secret = secret;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype _Nullable)parse:(NSDictionary *)dict {
|
||||
NSString *hostString = dict[@"host"];
|
||||
if (![hostString isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
NSString *host = hostString;
|
||||
|
||||
NSNumber *portNumber = dict[@"port"];
|
||||
if (![portNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
int32_t port = [portNumber intValue];
|
||||
|
||||
NSNumber *isMediaNumber = dict[@"isMedia"];
|
||||
if (![isMediaNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
bool isMedia = [isMediaNumber intValue] != 0;
|
||||
|
||||
NSString *secretString = dict[@"secret"];
|
||||
NSData *secret = nil;
|
||||
if ([secretString isKindOfClass:[NSString class]]) {
|
||||
secret = [[NSData alloc] initWithBase64EncodedString:secretString options:0];
|
||||
}
|
||||
|
||||
return [[AccountDatacenterAddress alloc] initWithHost:host port:port isMedia:isMedia secret:secret];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation AccountDatacenterInfo
|
||||
|
||||
- (instancetype)initWithMasterKey:(AccountDatacenterKey *)masterKey ephemeralMainKey:(AccountDatacenterKey * _Nullable)ephemeralMainKey ephemeralMediaKey:(AccountDatacenterKey * _Nullable)ephemeralMediaKey addressList:(NSArray<AccountDatacenterAddress *> *)addressList {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_masterKey = masterKey;
|
||||
_ephemeralMainKey = ephemeralMainKey;
|
||||
_ephemeralMediaKey = ephemeralMediaKey;
|
||||
_addressList = addressList;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype _Nullable)parse:(NSDictionary *)dict {
|
||||
NSDictionary *masterKeyDict = dict[@"masterKey"];
|
||||
if (![masterKeyDict isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
AccountDatacenterKey *masterKey = [AccountDatacenterKey parse:masterKeyDict];
|
||||
if (masterKey == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary *ephemeralMainKeyDict = dict[@"ephemeralMainKey"];
|
||||
AccountDatacenterKey *ephemeralMainKey = nil;
|
||||
if ([ephemeralMainKeyDict isKindOfClass:[NSDictionary class]]) {
|
||||
ephemeralMainKey = [AccountDatacenterKey parse:ephemeralMainKeyDict];
|
||||
}
|
||||
|
||||
NSDictionary *ephemeralMediaKeyDict = dict[@"ephemeralMediaKey"];
|
||||
AccountDatacenterKey *ephemeralMediaKey = nil;
|
||||
if ([ephemeralMediaKeyDict isKindOfClass:[NSDictionary class]]) {
|
||||
ephemeralMediaKey = [AccountDatacenterKey parse:ephemeralMediaKeyDict];
|
||||
}
|
||||
|
||||
NSArray *addressListArray = dict[@"addressList"];
|
||||
if (![addressListArray isKindOfClass:[NSArray class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray<AccountDatacenterAddress *> *addressList = [[NSMutableArray alloc] init];
|
||||
for (NSDictionary *addressListItem in addressListArray) {
|
||||
if (![addressListItem isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
AccountDatacenterAddress *address = [AccountDatacenterAddress parse:addressListItem];
|
||||
if (address == nil) {
|
||||
return nil;
|
||||
}
|
||||
[addressList addObject:address];
|
||||
}
|
||||
|
||||
return [[AccountDatacenterInfo alloc] initWithMasterKey:masterKey ephemeralMainKey:ephemeralMainKey ephemeralMediaKey:ephemeralMediaKey addressList:addressList];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation AccountProxyConnection
|
||||
|
||||
- (instancetype)initWithHost:(NSString *)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password secret:(NSData * _Nullable)secret {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_host = host;
|
||||
_port = port;
|
||||
username = _username;
|
||||
password = _password;
|
||||
secret = _secret;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype _Nullable)parse:(NSDictionary *)dict {
|
||||
NSString *hostString = dict[@"host"];
|
||||
if (![hostString isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
NSString *host = hostString;
|
||||
|
||||
NSNumber *portNumber = dict[@"port"];
|
||||
if (![portNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
int32_t port = [portNumber intValue];
|
||||
|
||||
NSString *usernameString = dict[@"username"];
|
||||
NSString *username = nil;
|
||||
if ([usernameString isKindOfClass:[NSString class]]) {
|
||||
username = usernameString;
|
||||
}
|
||||
|
||||
NSString *passwordString = dict[@"password"];
|
||||
NSString *password = nil;
|
||||
if ([passwordString isKindOfClass:[NSString class]]) {
|
||||
password = passwordString;
|
||||
}
|
||||
|
||||
NSString *secretString = dict[@"secret"];
|
||||
NSData *secret = nil;
|
||||
if ([secretString isKindOfClass:[NSString class]]) {
|
||||
secret = [[NSData alloc] initWithBase64EncodedString:secretString options:0];
|
||||
}
|
||||
|
||||
return [[AccountProxyConnection alloc] initWithHost:host port:port username:username password:password secret:secret];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation StoredAccountInfo
|
||||
|
||||
- (instancetype)initWithAccountId:(int64_t)accountId primaryId:(int32_t)primaryId isTestingEnvironment:(bool)isTestingEnvironment peerName:(NSString *)peerName datacenters:(NSDictionary<NSNumber *, AccountDatacenterInfo *> *)datacenters notificationKey:(AccountNotificationKey *)notificationKey {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_accountId = accountId;
|
||||
_primaryId = primaryId;
|
||||
_isTestingEnvironment = isTestingEnvironment;
|
||||
_peerName = peerName;
|
||||
_datacenters = datacenters;
|
||||
_notificationKey = notificationKey;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype _Nullable)parse:(NSDictionary *)dict {
|
||||
NSNumber *idNumber = dict[@"id"];
|
||||
if (![idNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
int64_t accountId = [idNumber longLongValue];
|
||||
|
||||
NSNumber *primaryIdNumber = dict[@"primaryId"];
|
||||
if (![primaryIdNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
int32_t primaryId = [primaryIdNumber intValue];
|
||||
|
||||
NSNumber *isTestingEnvironmentNumber = dict[@"isTestingEnvironment"];
|
||||
if (![isTestingEnvironmentNumber isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
bool isTestingEnvironment = [isTestingEnvironmentNumber intValue] != 0;
|
||||
|
||||
NSString *peerNameString = dict[@"peerName"];
|
||||
if (![peerNameString isKindOfClass:[NSString class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *peerName = peerNameString;
|
||||
|
||||
NSArray *datacentersArray = dict[@"datacenters"];
|
||||
if (![datacentersArray isKindOfClass:[NSArray class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableDictionary<NSNumber *, AccountDatacenterInfo *> *datacenters = [[NSMutableDictionary alloc] init];
|
||||
|
||||
for (NSInteger i = 0; i < datacentersArray.count; i += 2) {
|
||||
NSNumber *datacenterKey = datacentersArray[i];
|
||||
NSDictionary *datacenterData = datacentersArray[i + 1];
|
||||
|
||||
if (![datacenterKey isKindOfClass:[NSNumber class]]) {
|
||||
return nil;
|
||||
}
|
||||
if (![datacenterData isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
AccountDatacenterInfo *parsedDatacenter = [AccountDatacenterInfo parse:datacenterData];
|
||||
if (parsedDatacenter != nil) {
|
||||
datacenters[datacenterKey] = parsedDatacenter;
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *notificationKeyDict = dict[@"notificationKey"];
|
||||
if (![notificationKeyDict isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
AccountNotificationKey *notificationKey = [AccountNotificationKey parse:notificationKeyDict];
|
||||
if (notificationKey == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [[StoredAccountInfo alloc] initWithAccountId:accountId primaryId:primaryId isTestingEnvironment:isTestingEnvironment peerName:peerName datacenters:datacenters notificationKey:notificationKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation StoredAccountInfos
|
||||
|
||||
- (instancetype)initWithProxy:(AccountProxyConnection * _Nullable)proxy accounts:(NSArray<StoredAccountInfo *> *)accounts {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_proxy = proxy;
|
||||
_accounts = accounts;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (StoredAccountInfos * _Nullable)loadFromPath:(NSString *)path {
|
||||
NSData *data = [NSData dataWithContentsOfFile:path];
|
||||
if (data == nil) {
|
||||
return nil;
|
||||
}
|
||||
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
|
||||
if (![dict isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
AccountProxyConnection * _Nullable proxy = nil;
|
||||
NSDictionary *proxyDict = dict[@"proxy"];
|
||||
if ([proxyDict isKindOfClass:[NSDictionary class]]) {
|
||||
proxy = [AccountProxyConnection parse:proxyDict];
|
||||
}
|
||||
|
||||
NSMutableArray<StoredAccountInfo *> *accounts = [[NSMutableArray alloc] init];
|
||||
|
||||
NSArray *accountsObject = dict[@"accounts"];
|
||||
if ([accountsObject isKindOfClass:[NSArray class]]) {
|
||||
for (NSDictionary *object in accountsObject) {
|
||||
if ([object isKindOfClass:[NSDictionary class]]) {
|
||||
StoredAccountInfo *account = [StoredAccountInfo parse:object];
|
||||
if (account != nil) {
|
||||
[accounts addObject:account];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [[StoredAccountInfos alloc] initWithProxy:proxy accounts:accounts];;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static NSData *sha256Digest(NSData *data) {
|
||||
uint8_t digest[CC_SHA256_DIGEST_LENGTH];
|
||||
CC_SHA256(data.bytes, (CC_LONG)data.length, digest);
|
||||
|
||||
return [[NSData alloc] initWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
|
||||
}
|
||||
|
||||
static NSData *concatData(NSData *data1, NSData *data2) {
|
||||
NSMutableData *data = [[NSMutableData alloc] initWithCapacity:data1.length + data2.length];
|
||||
[data appendData:data1];
|
||||
[data appendData:data2];
|
||||
return data;
|
||||
}
|
||||
|
||||
static NSData *concatData3(NSData *data1, NSData *data2, NSData *data3) {
|
||||
NSMutableData *data = [[NSMutableData alloc] initWithCapacity:data1.length + data2.length + data3.length];
|
||||
[data appendData:data1];
|
||||
[data appendData:data2];
|
||||
[data appendData:data3];
|
||||
return data;
|
||||
}
|
||||
|
||||
NSDictionary * _Nullable decryptedNotificationPayload(NSArray<StoredAccountInfo *> *accounts, NSData *data, int *selectedAccountIndex) {
|
||||
if (data.length < 8 + 16) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
int accountIndex = -1;
|
||||
for (StoredAccountInfo *account in accounts) {
|
||||
accountIndex += 1;
|
||||
|
||||
AccountNotificationKey *notificationKey = account.notificationKey;
|
||||
if (![[data subdataWithRange:NSMakeRange(0, 8)] isEqualToData:notificationKey.keyId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int x = 8;
|
||||
NSData *msgKey = [data subdataWithRange:NSMakeRange(8, 16)];
|
||||
NSData *rawData = [data subdataWithRange:NSMakeRange(8 + 16, data.length - (8 + 16))];
|
||||
|
||||
NSData *sha256_a = sha256Digest(concatData(msgKey, [notificationKey.data subdataWithRange:NSMakeRange(x, 36)]));
|
||||
NSData *sha256_b = sha256Digest(concatData([notificationKey.data subdataWithRange:NSMakeRange(40 + x, 36)], msgKey));
|
||||
NSData *aesKey = concatData3([sha256_a subdataWithRange:NSMakeRange(0, 8)], [sha256_b subdataWithRange:NSMakeRange(8, 16)], [sha256_a subdataWithRange:NSMakeRange(24, 8)]);
|
||||
NSData *aesIv = concatData3([sha256_b subdataWithRange:NSMakeRange(0, 8)], [sha256_a subdataWithRange:NSMakeRange(8, 16)], [sha256_b subdataWithRange:NSMakeRange(24, 8)]);
|
||||
|
||||
NSData *decryptedData = MTAesDecrypt(rawData, aesKey, aesIv);
|
||||
if (decryptedData.length <= 4) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
int32_t dataLength = 0;
|
||||
[decryptedData getBytes:&dataLength range:NSMakeRange(0, 4)];
|
||||
|
||||
if (dataLength < 0 || dataLength > decryptedData.length - 4) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *checkMsgKeyLarge = sha256Digest(concatData([notificationKey.data subdataWithRange:NSMakeRange(88 + x, 32)], decryptedData));
|
||||
NSData *checkMsgKey = [checkMsgKeyLarge subdataWithRange:NSMakeRange(8, 16)];
|
||||
|
||||
if (![checkMsgKey isEqualToData:msgKey]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *contentData = [decryptedData subdataWithRange:NSMakeRange(4, dataLength)];
|
||||
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:contentData options:0 error:nil];
|
||||
if (![dict isKindOfClass:[NSDictionary class]]) {
|
||||
return nil;
|
||||
}
|
||||
if (selectedAccountIndex != nil) {
|
||||
*selectedAccountIndex = accountIndex;
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
@ -437,6 +437,7 @@ private struct NotificationContent: CustomStringConvertible {
|
||||
var category: String?
|
||||
var userInfo: [AnyHashable: Any] = [:]
|
||||
var attachments: [UNNotificationAttachment] = []
|
||||
var silent = false
|
||||
|
||||
var senderPerson: INPerson?
|
||||
var senderImage: INImage?
|
||||
@ -469,13 +470,18 @@ private struct NotificationContent: CustomStringConvertible {
|
||||
|
||||
self.senderImage = image
|
||||
|
||||
var displayName: String = peer.debugDisplayTitle
|
||||
if self.silent {
|
||||
displayName = "\(displayName) 🔕"
|
||||
}
|
||||
|
||||
var personNameComponents = PersonNameComponents()
|
||||
personNameComponents.nickname = peer.debugDisplayTitle
|
||||
|
||||
personNameComponents.nickname = displayName
|
||||
|
||||
self.senderPerson = INPerson(
|
||||
personHandle: INPersonHandle(value: "\(peer.id.toInt64())", type: .unknown),
|
||||
nameComponents: personNameComponents,
|
||||
displayName: peer.debugDisplayTitle,
|
||||
displayName: displayName,
|
||||
image: image,
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: "\(peer.id.toInt64())",
|
||||
@ -489,7 +495,11 @@ private struct NotificationContent: CustomStringConvertible {
|
||||
var content = UNMutableNotificationContent()
|
||||
|
||||
if let title = self.title {
|
||||
content.title = title
|
||||
if self.silent {
|
||||
content.title = "\(title) 🔕"
|
||||
} else {
|
||||
content.title = title
|
||||
}
|
||||
}
|
||||
if let subtitle = self.subtitle {
|
||||
content.subtitle = subtitle
|
||||
@ -567,6 +577,31 @@ private struct NotificationContent: CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
private func getCurrentRenderedTotalUnreadCount(accountManager: AccountManager<TelegramAccountManagerTypes>, postbox: Postbox) -> Signal<(Int32, RenderedTotalUnreadCountType), NoError> {
|
||||
let counters = postbox.transaction { transaction -> ChatListTotalUnreadState in
|
||||
return transaction.getTotalUnreadState(groupId: .root)
|
||||
}
|
||||
return combineLatest(
|
||||
accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings])
|
||||
|> take(1),
|
||||
counters
|
||||
)
|
||||
|> map { sharedData, totalReadCounters -> (Int32, RenderedTotalUnreadCountType) in
|
||||
let inAppSettings: InAppNotificationSettings
|
||||
if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.inAppNotificationSettings]?.get(InAppNotificationSettings.self) {
|
||||
inAppSettings = value
|
||||
} else {
|
||||
inAppSettings = .defaultSettings
|
||||
}
|
||||
let type: RenderedTotalUnreadCountType
|
||||
switch inAppSettings.totalUnreadCountDisplayStyle {
|
||||
case .filtered:
|
||||
type = .filtered
|
||||
}
|
||||
return (totalReadCounters.count(for: inAppSettings.totalUnreadCountDisplayStyle.category, in: inAppSettings.totalUnreadCountDisplayCategory.statsType, with: inAppSettings.totalUnreadCountIncludeTags), type)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, iOS 10.0, *)
|
||||
private final class NotificationServiceHandler {
|
||||
private let queue: Queue
|
||||
@ -912,9 +947,7 @@ private final class NotificationServiceHandler {
|
||||
|
||||
if let silentString = payloadJson["silent"] as? String {
|
||||
if let silentValue = Int(silentString), silentValue != 0 {
|
||||
if let title = content.title {
|
||||
content.title = "\(title) 🔕"
|
||||
}
|
||||
content.silent = true
|
||||
}
|
||||
}
|
||||
if var attachmentDataString = payloadJson["attachb64"] as? String {
|
||||
@ -1062,7 +1095,7 @@ private final class NotificationServiceHandler {
|
||||
if let resource = fetchResource {
|
||||
if let _ = strongSelf.stateManager?.postbox.mediaBox.completedResourcePath(resource) {
|
||||
} else {
|
||||
let intervals: Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError> = .single([(0 ..< Int(Int32.max), MediaBoxFetchPriority.maximum)])
|
||||
let intervals: Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError> = .single([(0 ..< Int64.max, MediaBoxFetchPriority.maximum)])
|
||||
fetchMediaSignal = Signal { subscriber in
|
||||
let collectedData = Atomic<Data>(value: Data())
|
||||
return standaloneMultipartFetch(
|
||||
@ -1113,7 +1146,7 @@ private final class NotificationServiceHandler {
|
||||
if let path = strongSelf.stateManager?.postbox.mediaBox.completedResourcePath(resource), let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
|
||||
fetchNotificationSoundSignal = .single(data)
|
||||
} else {
|
||||
let intervals: Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError> = .single([(0 ..< Int(Int32.max), MediaBoxFetchPriority.maximum)])
|
||||
let intervals: Signal<[(Range<Int64>, MediaBoxFetchPriority)], NoError> = .single([(0 ..< Int64.max, MediaBoxFetchPriority.maximum)])
|
||||
fetchNotificationSoundSignal = Signal { subscriber in
|
||||
let collectedData = Atomic<Data>(value: Data())
|
||||
return standaloneMultipartFetch(
|
||||
|
||||
@ -45,7 +45,7 @@ class ShareRootController: UIViewController {
|
||||
|
||||
let appVersion = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "unknown"
|
||||
|
||||
self.impl = ShareRootControllerImpl(initializationData: ShareRootControllerInitializationData(appBundleId: baseAppBundleId, appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), getExtensionContext: { [weak self] in
|
||||
self.impl = ShareRootControllerImpl(initializationData: ShareRootControllerInitializationData(appBundleId: baseAppBundleId, appBuildType: buildConfig.isAppStoreBuild ? .public : .internal, appGroupPath: appGroupUrl.path, apiId: buildConfig.apiId, apiHash: buildConfig.apiHash, languagesCategory: languagesCategory, encryptionParameters: encryptionParameters, appVersion: appVersion, bundleData: buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), getExtensionContext: { [weak self] in
|
||||
return self?.extensionContext
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,104 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon@120px.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "icon@180px.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon@76px.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon@152px.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "icon@167px.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "icon@1024px.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 370 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 665 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
@ -1,119 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BlueNotificationIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BlueNotificationIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Simple@58x58.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Simple@87x87.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "Simple@80x80.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BlueIcon@2x-1.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BlueIcon@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "BlueIcon@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BlueNotificationIcon.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BlueNotificationIcon@2x-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Simple@29x29.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Simple@58x58-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Simple@40x40-1.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "Simple@80x80-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BlueIconIpad.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BlueIconIpad@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "BlueIconLargeIpad@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "Simple-iTunesArtwork.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
},
|
||||
"properties" : {
|
||||
"pre-rendered" : true
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
@ -1,12 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ic_lt_user.pdf"
|
||||
"filename" : "ic_lt_user.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Telegram/Telegram-iOS/Premium.alticon/Premium@2x.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
Telegram/Telegram-iOS/Premium.alticon/Premium@3x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@2x.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
Telegram/Telegram-iOS/PremiumBlack.alticon/PremiumBlack@3x.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@2x.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
Telegram/Telegram-iOS/PremiumTurbo.alticon/PremiumTurbo@3x.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 12 KiB |
@ -388,7 +388,7 @@
|
||||
"Tour.Text5" = "**Telegram** lets you access your\nmessages from multiple devices.";
|
||||
|
||||
"Tour.Title6" = "Free";
|
||||
"Tour.Text6" = "**Telegram** is free forever. No ads.\nNo subscription fees.";
|
||||
"Tour.Text6" = "**Telegram** isprovides free unlimited cloud storage\nfor chats and media.";
|
||||
|
||||
"Tour.StartButton" = "Start Messaging";
|
||||
|
||||
@ -2494,6 +2494,13 @@ Unused sets are archived when you add more.";
|
||||
|
||||
"Message.PaymentSent" = "Payment: %@";
|
||||
"Notification.PaymentSent" = "You have just successfully transferred {amount} to {name} for {title}";
|
||||
"Notification.PaymentSentNoTitle" = "You have just successfully transferred {amount} to {name}";
|
||||
|
||||
"Notification.PaymentSentRecurringInit" = "You have just successfully transferred {amount} to {name} for {title} and allowed future reccurrent payments";
|
||||
"Notification.PaymentSentRecurringInitNoTitle" = "You have just successfully transferred {amount} to {name} and allowed future reccurrent payments";
|
||||
|
||||
"Notification.PaymentSentRecurringUsed" = "You have just successfully transferred {amount} to {name} for {title} via recurrent payments";
|
||||
"Notification.PaymentSentRecurringUsedNoTitle" = "You have just successfully transferred {amount} to {name} via recurrent payments";
|
||||
|
||||
"Common.NotNow" = "Not Now";
|
||||
|
||||
@ -6861,6 +6868,7 @@ Sorry for the inconvenience.";
|
||||
"Chat.NavigationNoChannels" = "You have no unread channels";
|
||||
|
||||
"Message.SponsoredLabel" = "sponsored";
|
||||
"Message.RecommendedLabel" = "recommended";
|
||||
|
||||
"Stickers.Favorites" = "Favorites";
|
||||
"Stickers.Recent" = "Recent";
|
||||
@ -7526,3 +7534,163 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Share.UploadProgress" = "Uploading • %d%";
|
||||
"Share.UploadDone" = "Done";
|
||||
|
||||
"StickerPack.Share" = "Share Stickers";
|
||||
"StickerPack.CopyLink" = "Copy Link";
|
||||
|
||||
"Stickers.PremiumStickers" = "Premium Stickers";
|
||||
|
||||
"Channel.AddUserKickedError" = "Sorry, you can't add this user because they are on the list of Removed Users and you can't unban them.";
|
||||
"Channel.AddAdminKickedError" = "Sorry, you can't add this user as an admin because they are in the Removed Users list and you can't unban them.";
|
||||
|
||||
"Premium.Stickers.Description" = "Unlock this sticker and more by subscribing to Telegram Premium.";
|
||||
"Premium.Stickers.Proceed" = "Unlock Premium Stickers";
|
||||
|
||||
"Premium.Reactions.Proceed" = "Unlock Premium Reactions";
|
||||
|
||||
"Premium.AppIcons.Proceed" = "Unlock Premium Icons";
|
||||
|
||||
"AccessDenied.LocationPreciseDenied" = "To share your specific location in this chat, please go to Settings > Privacy > Location Services > Telegram and set Precise Location to On.";
|
||||
|
||||
"Chat.MultipleTypingPair" = "%@ and %@";
|
||||
"Chat.MultipleTypingMore" = "%@ and %@ others";
|
||||
|
||||
"Group.Username.RemoveExistingUsernamesOrExtendInfo" = "You have reserved too many public links. Try revoking a link from an older group or channel, or upgrade to **Telegram Premium** to double the limit to **%@** public links.";
|
||||
"Group.Username.RemoveExistingUsernamesFinalInfo" = "You have reserved too many public links. Try revoking the link from an older group or channel, or create a private one instead.";
|
||||
|
||||
"OldChannels.TooManyCommunitiesText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one or upgrade to **Telegram Premium** to double the limit to **%@** groups and channels.";
|
||||
"OldChannels.TooManyCommunitiesFinalText" = "You are a member of **%@** groups and channels. Please leave some before joining a new one.";
|
||||
"OldChannels.LeaveCommunities_1" = "Leave %@ Community";
|
||||
"OldChannels.LeaveCommunities_any" = "Leave %@ Communities";
|
||||
|
||||
"Premium.FileTooLarge" = "File Too Large";
|
||||
"Premium.LimitReached" = "Limit Reached";
|
||||
"Premium.IncreaseLimit" = "Increase Limit";
|
||||
|
||||
"Premium.MaxFoldersCountText" = "You have reached the limit of **%1$@** folders. You can double the limit to **%2$@** folders by subscribing to **Telegram Premium**.";
|
||||
"Premium.MaxFoldersCountFinalText" = "Sorry, you can't create more than **%1$@** folders.";
|
||||
"Premium.MaxChatsInFolderText" = "Sorry, you can't add more than **%1$@** chats to a folder. You can increase this limit to **%2$@** by upgrading to **Telegram Premium**.";
|
||||
"Premium.MaxChatsInFolderFinalText" = "Sorry, you can't add more than **%@** chats to a folder.";
|
||||
"Premium.MaxFileSizeText" = "Double this limit to %@ per file by subscribing to **Telegram Premium**.";
|
||||
"Premium.MaxFileSizeFinalText" = "The document can't be sent, because it is larger than **%@**.";
|
||||
"Premium.MaxPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some of the currently pinned ones or subscribe to **Telegram Premium** to double the limit to **%2$@** chats.";
|
||||
"Premium.MaxPinsFinalText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some of the currently pinned ones.";
|
||||
"Premium.MaxFavedStickersTitle" = "The Limit of %@ Stickers Reached";
|
||||
"Premium.MaxFavedStickersText" = "An older sticker was replaced with this one. You can [increase the limit]() to %@ stickers.";
|
||||
"Premium.MaxFavedStickersFinalText" = "An older sticker was replaced with this one.";
|
||||
"Premium.MaxSavedGifsTitle" = "The Limit of %@ GIFs Reached";
|
||||
"Premium.MaxSavedGifsText" = "An older GIF was replaced with this one. You can [increase the limit]() to %@ GIFS.";
|
||||
"Premium.MaxSavedGifsFinalText" = "An older GIF was replaced with this one.";
|
||||
"Premium.MaxAccountsText" = "You have reached the limit of **%@** connected accounts. You can free one place by subscribing to **Telegram Premium**.";
|
||||
"Premium.MaxAccountsFinalText" = "You have reached the limit of **%@** connected accounts.";
|
||||
|
||||
"Premium.Free" = "Free";
|
||||
"Premium.Premium" = "Premium";
|
||||
|
||||
"Premium.Title" = "Telegram Premium";
|
||||
"Premium.Description" = "Go **beyond the limits**, get **exclusive features** and support us by subscribing to **Telegram Premium**.";
|
||||
|
||||
"Premium.PersonalTitle" = "[%@]() is a subscriber\nof Telegram Premium";
|
||||
"Premium.PersonalDescription" = "Owners of **Telegram Premium** accounts have exclusive access to multiple additional features.";
|
||||
|
||||
"Premium.SubscribedTitle" = "You are all set!";
|
||||
"Premium.SubscribedDescription" = "Thank you for subsribing to **Telegram Premium**. Here's what is now unlocked.";
|
||||
|
||||
"Premium.DoubledLimits" = "Doubled Limits";
|
||||
"Premium.DoubledLimitsInfo" = "Up to 1000 channels, 20 folders, 10 pins, 20 public links, 4 accounts and more.";
|
||||
|
||||
"Premium.UploadSize" = "4 GB Upload Size";
|
||||
"Premium.UploadSizeInfo" = "Increased upload size from 2 GB to 4 GB per document, unlimited storage overall.";
|
||||
|
||||
"Premium.FasterSpeed" = "Faster Download Speed";
|
||||
"Premium.FasterSpeedInfo" = "No more limits on the speed with which media and documents are downloaded.";
|
||||
|
||||
"Premium.VoiceToText" = "Voice-to-Text Conversion";
|
||||
"Premium.VoiceToTextInfo" = "Ability to read the transcript of any incoming voice message.";
|
||||
|
||||
"Premium.NoAds" = "No Ads";
|
||||
"Premium.NoAdsInfo" = "No more ads in public channels where Telegram sometimes shows ads.";
|
||||
|
||||
"Premium.Reactions" = "Unique Reactions";
|
||||
"Premium.ReactionsInfo" = "Additional animated reactions on messages, available only to the Premium subscribers.";
|
||||
|
||||
"Premium.ReactionsStandalone" = "Additional Reactions";
|
||||
"Premium.ReactionsStandaloneInfo" = "Unlock a wider range of reactions by subscribing to **Telegram Premium**.";
|
||||
|
||||
"Premium.Stickers" = "Premium Stickers";
|
||||
"Premium.StickersInfo" = "Exclusive enlarged stickers featuring additional effects, updated monthly.";
|
||||
|
||||
"Premium.ChatManagement" = "Advanced Chat Management";
|
||||
"Premium.ChatManagementInfo" = "Tools to set default folder, auto-archive and hide new chats.";
|
||||
|
||||
"Premium.Badge" = "Profile Badge";
|
||||
"Premium.BadgeInfo" = "A badge next to your name showing that you are helping support Telegram.";
|
||||
|
||||
"Premium.Avatar" = "Animated Profile Pictures";
|
||||
"Premium.AvatarInfo" = "Video avatars animated in chat lists and chats to allow for additional self-expression.";
|
||||
|
||||
"Premium.AppIcon" = "Telegram App Icon";
|
||||
"Premium.AppIconInfo" = "Choose from a selection of Telegram app icons for your homescreen.";
|
||||
|
||||
"Premium.AppIconStandalone" = "Additional App Icons";
|
||||
"Premium.AppIconStandaloneInfo" = "Unlock a wider range of app icons by subscribing to **Telegram Premium**.";
|
||||
|
||||
"Premium.SubscribeFor" = "Subscribe for %@ / month";
|
||||
|
||||
"Premium.AboutTitle" = "ABOUT TELEGRAM PREMIUM";
|
||||
"Premium.AboutText" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone.";
|
||||
|
||||
"Premium.Terms" = "By purchasing a Premium subscription, you agree to our [Terms of Service](terms) and [Privacy Policy](privacy).";
|
||||
|
||||
"Premium.ChargeInfo" = "Next charge: %1$@ on %2$@. [Cancel](cancel).";
|
||||
|
||||
"Conversation.CopyProtectionSavingDisabledSecret" = "Saving is restricted";
|
||||
"Conversation.CopyProtectionForwardingDisabledSecret" = "Forwards are restricted";
|
||||
|
||||
"Settings.Terms_URL" = "https://telegram.org/tos";
|
||||
"Settings.PrivacyPolicy_URL" = "https://telegram.org/privacy";
|
||||
|
||||
"Stickers.PremiumPackInfoText" = "This pack contains premium stickers like this one.";
|
||||
"Stickers.PremiumPackView" = "View";
|
||||
|
||||
"Conversation.PremiumUploadFileTooLarge" = "File could not be sent, because it is larger than 4 GB.\n\nYou can send as many files as you like, but each must be smaller than 4 GB.";
|
||||
|
||||
"SponsoredMessageMenu.Hide" = "Hide";
|
||||
|
||||
"ChatListFolder.MaxChatsInFolder" = "Sorry, you can't add more than %d chats to a folder.";
|
||||
|
||||
"Conversation.SaveGif" = "Save GIF";
|
||||
|
||||
"Premium.Limits.Title" = "Doubled Limits";
|
||||
"Premium.Limits.GroupsAndChannels" = "Groups and Channels";
|
||||
"Premium.Limits.PinnedChats" = "Pinned Chats";
|
||||
"Premium.Limits.PublicLinks" = "Public Links";
|
||||
"Premium.Limits.SavedGifs" = "Saved GIFs";
|
||||
"Premium.Limits.FavedStickers" = "Favorite Stickers";
|
||||
"Premium.Limits.Bio" = "Bio";
|
||||
"Premium.Limits.Captions" = "Captions";
|
||||
"Premium.Limits.Folders" = "Folders";
|
||||
"Premium.Limits.ChatsPerFolder" = "Chats per Folder";
|
||||
"Premium.Limits.Accounts" = "Connected Accounts";
|
||||
|
||||
"Premium.Limits.GroupsAndChannelsInfo" = "Join up to 1000 channels and large groups";
|
||||
"Premium.Limits.PinnedChatsInfo" = "Pin up to 10 chats in your main chat list";
|
||||
"Premium.Limits.PublicLinksInfo" = "Reserve up to 20 [t.me/name]() links";
|
||||
"Premium.Limits.SavedGifsInfo" = "Save up to 400 GIFs in your Favorite GIFs";
|
||||
"Premium.Limits.FavedStickersInfo" = "Save up to 10 stickers in your Favorite stickers";
|
||||
"Premium.Limits.BioInfo" = "Add more symbols and use links in your bio";
|
||||
"Premium.Limits.CaptionsInfo" = "Use longer descriptions for your photos and videos";
|
||||
"Premium.Limits.FoldersInfo" = "Organize your chats into 20 folders";
|
||||
"Premium.Limits.ChatsPerFolderInfo" = "Add up to 200 chats into each of your folders";
|
||||
"Premium.Limits.AccountsInfo" = "Connect 4 accounts with different mobile numbers";
|
||||
|
||||
"WebApp.Settings" = "Settings";
|
||||
|
||||
"Bot.AccepRecurrentInfo" = "I accept [Terms of Service]() of **%1$@**";
|
||||
|
||||
"Chat.AudioTranscriptionRateAction" = "Rate Transcription";
|
||||
"Chat.AudioTranscriptionFeedbackTip" = "Thank you for your feedback.";
|
||||
"Message.AudioTranscription.ErrorEmpty" = "No speech detected";
|
||||
"Message.AudioTranscription.ErrorTooLong" = "The audio is too long";
|
||||
|
||||
"WebApp.SelectChat" = "Select Chat";
|
||||
|
||||
@ -53,8 +53,8 @@ bazel_skylib_workspace()
|
||||
|
||||
http_file(
|
||||
name = "cmake_tar_gz",
|
||||
urls = ["https://github.com/Kitware/CMake/releases/download/v3.19.2/cmake-3.19.2-macos-universal.tar.gz"],
|
||||
sha256 = "50afa2cb66bea6a0314ef28034f3ff1647325e30cf5940f97906a56fd9640bd8",
|
||||
urls = ["https://github.com/Kitware/CMake/releases/download/v3.23.1/cmake-3.23.1-macos-universal.tar.gz"],
|
||||
sha256 = "f794ed92ccb4e9b6619a77328f313497d7decf8fb7e047ba35a348b838e0e1e2",
|
||||
)
|
||||
|
||||
http_archive(
|
||||
|
||||
BIN
build-system/AppleWWDRCAG3.cer
Normal file
@ -8,6 +8,7 @@ telegram_is_internal_build = "false"
|
||||
telegram_is_appstore_build = "true"
|
||||
telegram_appstore_id = "686449807"
|
||||
telegram_app_specific_url_scheme = "tg"
|
||||
telegram_premium_iap_product_id = ""
|
||||
telegram_aps_environment = "production"
|
||||
telegram_enable_siri = True
|
||||
telegram_enable_icloud = True
|
||||
|
||||
@ -35,6 +35,7 @@ prepare_build_variables () {
|
||||
IS_APPSTORE_BUILD \
|
||||
APPSTORE_ID \
|
||||
APP_SPECIFIC_URL_SCHEME \
|
||||
PREMIUM_IAP_PRODUCT_ID \
|
||||
TELEGRAM_DISABLE_EXTENSIONS \
|
||||
)
|
||||
|
||||
@ -66,6 +67,7 @@ prepare_build_variables () {
|
||||
echo "telegram_is_appstore_build = \"$IS_APPSTORE_BUILD\"" >> "$VARIABLES_PATH"
|
||||
echo "telegram_appstore_id = \"$APPSTORE_ID\"" >> "$VARIABLES_PATH"
|
||||
echo "telegram_app_specific_url_scheme = \"$APP_SPECIFIC_URL_SCHEME\"" >> "$VARIABLES_PATH"
|
||||
echo "telegram_premium_iap_product_id = \"$PREMIUM_IAP_PRODUCT_ID\"" >> "$VARIABLES_PATH"
|
||||
echo "telegram_aps_environment = \"$APS_ENVIRONMENT\"" >> "$VARIABLES_PATH"
|
||||
|
||||
if [ "$TELEGRAM_DISABLE_EXTENSIONS" == "1" ]; then
|
||||
|
||||
@ -15,6 +15,7 @@ export IS_INTERNAL_BUILD="false"
|
||||
export IS_APPSTORE_BUILD="true"
|
||||
export APPSTORE_ID="686449807"
|
||||
export APP_SPECIFIC_URL_SCHEME="tgapp"
|
||||
export PREMIUM_IAP_PRODUCT_ID="org.telegram.telegramPremium.monthly"
|
||||
|
||||
if [ -z "$BUILD_NUMBER" ]; then
|
||||
echo "BUILD_NUMBER is not defined"
|
||||
|
||||
141
buildbox/build-telegram-next.sh
Normal file
@ -0,0 +1,141 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
MACOS_VERSION="12"
|
||||
XCODE_VERSION="13.2.1"
|
||||
GUEST_SHELL="bash"
|
||||
|
||||
if [ -z "$VIRTUALBUILD_HOST" ]; then
|
||||
echo "VIRTUALBUILD_HOST is not defined"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VM_BASE_NAME="macos$(echo $MACOS_VERSION | sed -e 's/\.'/_/g)-Xcode$(echo $XCODE_VERSION | sed -e 's/\.'/_/g)"
|
||||
echo "Base VM: \"$VM_BASE_NAME\""
|
||||
|
||||
if [ -z "$BAZEL" ]; then
|
||||
echo "BAZEL is not defined"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$BAZEL" ]; then
|
||||
echo "bazel not found at $BAZEL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILDBOX_DIR="buildbox"
|
||||
|
||||
mkdir -p "$BUILDBOX_DIR/transient-data"
|
||||
|
||||
rm -f "tools/bazel"
|
||||
cp "$BAZEL" "tools/bazel"
|
||||
|
||||
BUILD_CONFIGURATION="$1"
|
||||
|
||||
if [ "$BUILD_CONFIGURATION" == "hockeyapp" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental-2" ] || [ "$BUILD_CONFIGURATION" == "App Store-development" ]; then
|
||||
CODESIGNING_SUBPATH="$BUILDBOX_DIR/transient-data/telegram-codesigning/codesigning"
|
||||
elif [ "$BUILD_CONFIGURATION" == "appstore" ]; then
|
||||
CODESIGNING_SUBPATH="$BUILDBOX_DIR/transient-data/telegram-codesigning/codesigning"
|
||||
elif [ "$BUILD_CONFIGURATION" == "verify" ]; then
|
||||
CODESIGNING_SUBPATH="build-system/fake-codesigning"
|
||||
else
|
||||
echo "Unknown configuration $1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
COMMIT_COMMENT="$(git log -1 --pretty=%B)"
|
||||
case "$COMMIT_COMMENT" in
|
||||
*"[nocache]"*)
|
||||
export BAZEL_HTTP_CACHE_URL=""
|
||||
;;
|
||||
esac
|
||||
|
||||
COMMIT_ID="$(git rev-parse HEAD)"
|
||||
COMMIT_AUTHOR=$(git log -1 --pretty=format:'%an')
|
||||
if [ -z "$2" ]; then
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
BUILD_NUMBER_OFFSET="$(cat build_number_offset)"
|
||||
COMMIT_COUNT="$(($COMMIT_COUNT+$BUILD_NUMBER_OFFSET))"
|
||||
BUILD_NUMBER="$COMMIT_COUNT"
|
||||
else
|
||||
BUILD_NUMBER="$2"
|
||||
fi
|
||||
|
||||
BASE_DIR=$(pwd)
|
||||
|
||||
if [ "$BUILD_CONFIGURATION" == "hockeyapp" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental-2" ] || [ "$BUILD_CONFIGURATION" == "appstore" ] || [ "$BUILD_CONFIGURATION" == "appstore-development" ]; then
|
||||
if [ ! `which generate-configuration.sh` ]; then
|
||||
echo "generate-configuration.sh not found in PATH $PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning"
|
||||
mkdir -p "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
|
||||
|
||||
case "$BUILD_CONFIGURATION" in
|
||||
"hockeyapp"|"appcenter-experimental"|"appcenter-experimental-2")
|
||||
generate-configuration.sh internal release "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning" "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
|
||||
;;
|
||||
|
||||
"appstore")
|
||||
generate-configuration.sh appstore release "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning" "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
|
||||
;;
|
||||
|
||||
"appstore-development")
|
||||
generate-configuration.sh appstore development "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning" "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown build configuration $BUILD_CONFIGURATION"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
elif [ "$BUILD_CONFIGURATION" == "verify" ]; then
|
||||
mkdir -p "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning"
|
||||
mkdir -p "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
|
||||
|
||||
cp -R build-system/fake-codesigning/* "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning/"
|
||||
cp -R build-system/example-configuration/* "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration/"
|
||||
fi
|
||||
|
||||
if [ ! -d "$CODESIGNING_SUBPATH" ]; then
|
||||
echo "$CODESIGNING_SUBPATH does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_DIR=$(basename "$BASE_DIR")
|
||||
rm -f "$BUILDBOX_DIR/transient-data/source.tar"
|
||||
set -x
|
||||
find . -type f -a -not -regex "\\." -a -not -regex ".*\\./git" -a -not -regex ".*\\./git/.*" -a -not -regex "\\./bazel-bin" -a -not -regex "\\./bazel-bin/.*" -a -not -regex "\\./bazel-out" -a -not -regex "\\./bazel-out/.*" -a -not -regex "\\./bazel-testlogs" -a -not -regex "\\./bazel-testlogs/.*" -a -not -regex "\\./bazel-telegram-ios" -a -not -regex "\\./bazel-telegram-ios/.*" -a -not -regex "\\./buildbox" -a -not -regex "\\./buildbox/.*" -a -not -regex "\\./buck-out" -a -not -regex "\\./buck-out/.*" -a -not -regex "\\./\\.buckd" -a -not -regex "\\./\\.buckd/.*" -a -not -regex "\\./build" -a -not -regex "\\./build/.*" -print0 | tar cf "$BUILDBOX_DIR/transient-data/source.tar" --null -T -
|
||||
|
||||
PROCESS_ID="$$"
|
||||
|
||||
initialization_params="$VM_BASE_NAME"
|
||||
initialization_params="$initialization_params&watchpid=$PROCESS_ID"
|
||||
|
||||
ssh_credentials=$(curl --fail --insecure "https://$VIRTUALBUILD_HOST/run-image?name=$initialization_params")
|
||||
|
||||
ssh_username=$(echo "$ssh_credentials" | python3 -c "import sys, json; print(json.load(sys.stdin)['sshCredentials']['username'])")
|
||||
ssh_host=$(echo "$ssh_credentials" | python3 -c "import sys, json; print(json.load(sys.stdin)['sshCredentials']['host'])")
|
||||
ssh_privateKey=$(echo "$ssh_credentials" | python3 -c "import sys, json; print(json.load(sys.stdin)['sshCredentials']['privateKey'])")
|
||||
|
||||
ssh_privateKeyFile=$(mktemp)
|
||||
echo "$ssh_privateKey" | base64 --decode > "$ssh_privateKeyFile"
|
||||
|
||||
scp -i "$ssh_privateKeyFile" -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$CODESIGNING_SUBPATH" $ssh_username@"$ssh_host":codesigning_data
|
||||
scp -i "$ssh_privateKeyFile" -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration" $ssh_username@"$ssh_host":telegram-configuration
|
||||
|
||||
scp -i "$ssh_privateKeyFile" -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$BUILDBOX_DIR/guest-build-telegram.sh" "$BUILDBOX_DIR/transient-data/source.tar" $ssh_username@"$ssh_host":
|
||||
|
||||
ssh -i "$ssh_privateKeyFile" -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $ssh_username@"$ssh_host" -o ServerAliveInterval=60 -t "export BUILD_NUMBER=\"$BUILD_NUMBER\"; export BAZEL_HTTP_CACHE_URL=\"$BAZEL_HTTP_CACHE_URL\"; $GUEST_SHELL -l guest-build-telegram.sh $BUILD_CONFIGURATION" || true
|
||||
|
||||
OUTPUT_PATH="build/artifacts"
|
||||
rm -rf "$OUTPUT_PATH"
|
||||
mkdir -p "$OUTPUT_PATH"
|
||||
|
||||
scp -i "$ssh_privateKeyFile" -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr $ssh_username@"$ssh_host":"telegram-ios/build/artifacts/*" "$OUTPUT_PATH/"
|
||||
|
||||
if [ ! -f "$OUTPUT_PATH/Telegram.ipa" ]; then
|
||||
exit 1
|
||||
fi
|
||||
@ -4,8 +4,8 @@ set -e
|
||||
|
||||
BUILD_TELEGRAM_VERSION="1"
|
||||
|
||||
MACOS_VERSION="11"
|
||||
XCODE_VERSION="13.2.1"
|
||||
MACOS_VERSION="12"
|
||||
XCODE_VERSION="13.4.1"
|
||||
GUEST_SHELL="bash"
|
||||
|
||||
VM_BASE_NAME="macos$(echo $MACOS_VERSION | sed -e 's/\.'/_/g)_Xcode$(echo $XCODE_VERSION | sed -e 's/\.'/_/g)"
|
||||
@ -59,7 +59,7 @@ BUILD_CONFIGURATION="$1"
|
||||
|
||||
if [ "$BUILD_CONFIGURATION" == "hockeyapp" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental-2" ]; then
|
||||
CODESIGNING_SUBPATH="$BUILDBOX_DIR/transient-data/telegram-codesigning/codesigning"
|
||||
elif [ "$BUILD_CONFIGURATION" == "appstore" ]; then
|
||||
elif [ "$BUILD_CONFIGURATION" == "appstore" ] || [ "$BUILD_CONFIGURATION" == "appstore-development" ]; then
|
||||
CODESIGNING_SUBPATH="$BUILDBOX_DIR/transient-data/telegram-codesigning/codesigning"
|
||||
elif [ "$BUILD_CONFIGURATION" == "verify" ]; then
|
||||
CODESIGNING_SUBPATH="build-system/fake-codesigning"
|
||||
@ -88,7 +88,7 @@ fi
|
||||
|
||||
BASE_DIR=$(pwd)
|
||||
|
||||
if [ "$BUILD_CONFIGURATION" == "hockeyapp" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental-2" ] || [ "$BUILD_CONFIGURATION" == "appstore" ]; then
|
||||
if [ "$BUILD_CONFIGURATION" == "hockeyapp" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental" ] || [ "$BUILD_CONFIGURATION" == "appcenter-experimental-2" ] || [ "$BUILD_CONFIGURATION" == "appstore" ] || [ "$BUILD_CONFIGURATION" == "appstore-development" ]; then
|
||||
if [ ! `which generate-configuration.sh` ]; then
|
||||
echo "generate-configuration.sh not found in PATH $PATH"
|
||||
exit 1
|
||||
@ -106,6 +106,10 @@ if [ "$BUILD_CONFIGURATION" == "hockeyapp" ] || [ "$BUILD_CONFIGURATION" == "app
|
||||
generate-configuration.sh appstore release "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning" "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
|
||||
;;
|
||||
|
||||
"appstore-development")
|
||||
generate-configuration.sh appstore development "$BASE_DIR/$BUILDBOX_DIR/transient-data/telegram-codesigning" "$BASE_DIR/$BUILDBOX_DIR/transient-data/build-configuration"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown build configuration $BUILD_CONFIGURATION"
|
||||
exit 1
|
||||
|
||||
@ -6,7 +6,11 @@ set -x
|
||||
IPA_PATH="build/artifacts/Telegram.ipa"
|
||||
DSYM_PATH="build/artifacts/Telegram.DSYMs.zip"
|
||||
|
||||
APPCENTER="/usr/local/bin/appcenter"
|
||||
if [ `which appcenter` ]; then
|
||||
APPCENTER="$(which appcenter)"
|
||||
else
|
||||
APPCENTER="/usr/local/bin/appcenter"
|
||||
fi
|
||||
|
||||
$APPCENTER login --token "$API_TOKEN"
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ else
|
||||
BUILD_NUMBER="$2"
|
||||
fi
|
||||
|
||||
if [ "$CONFIGURATION" == "hockeyapp" ] || [ "$CONFIGURATION" == "appcenter-experimental" ] || [ "$CONFIGURATION" == "appcenter-experimental-2" ]; then
|
||||
if [ "$CONFIGURATION" == "hockeyapp" ] || [ "$CONFIGURATION" == "appcenter-experimental" ] || [ "$CONFIGURATION" == "appcenter-experimental-2" ] || [ "$CONFIGURATION" == "appstore-development" ]; then
|
||||
FASTLANE_PASSWORD=""
|
||||
FASTLANE_ITC_TEAM_NAME=""
|
||||
elif [ "$CONFIGURATION" == "appstore" ]; then
|
||||
@ -69,6 +69,6 @@ if [ "$1" == "appstore" ]; then
|
||||
FASTLANE_PASSWORD="$FASTLANE_PASSWORD" xcrun altool --upload-app --type ios --file "$IPA_PATH" --username "$FASTLANE_ITC_USERNAME" --password "@env:FASTLANE_PASSWORD"
|
||||
elif [ "$1" == "hockeyapp" ]; then
|
||||
API_USER_NAME="$API_USER_NAME" API_APP_NAME="$API_APP_NAME" API_TOKEN="$API_TOKEN" sh buildbox/deploy-appcenter.sh
|
||||
elif [ "$1" == "appcenter-experimental" ] || [ "$1" == "appcenter-experimental-2" ]; then
|
||||
elif [ "$1" == "appcenter-experimental" ] || [ "$1" == "appcenter-experimental-2" ] || [ "$1" == "appstore-development" ]; then
|
||||
API_USER_NAME="$API_USER_NAME" API_APP_NAME="$API_APP_NAME" API_TOKEN="$API_TOKEN" sh buildbox/deploy-appcenter.sh
|
||||
fi
|
||||
|
||||
@ -13,6 +13,8 @@ elif [ "$1" == "testinghockeyapp-local" ]; then
|
||||
CERTS_PATH="$HOME/codesigning_data/certs/enterprise"
|
||||
elif [ "$1" == "appstore" ]; then
|
||||
CERTS_PATH="$HOME/codesigning_data/certs/distribution"
|
||||
elif [ "$1" == "appstore-development" ]; then
|
||||
CERTS_PATH="$HOME/codesigning_data/certs/development"
|
||||
elif [ "$1" == "verify" ]; then
|
||||
CERTS_PATH="$HOME/codesigning_data/certs/distribution"
|
||||
else
|
||||
@ -73,12 +75,15 @@ for f in "$CERTS_PATH"/*.p12; do
|
||||
done
|
||||
|
||||
for f in "$CERTS_PATH"/*.cer; do
|
||||
sudo security add-trusted-cert -d -r trustRoot -p codeSign -k "$MY_KEYCHAIN" "$f"
|
||||
#sudo security add-trusted-cert -d -r trustRoot -p codeSign -k "$MY_KEYCHAIN" "$f"
|
||||
security import "$f" -k "$MY_KEYCHAIN" -P "" -T /usr/bin/codesign -T /usr/bin/security
|
||||
done
|
||||
|
||||
security import "build-system/AppleWWDRCAG3.cer" -k "$MY_KEYCHAIN" -P "" -T /usr/bin/codesign -T /usr/bin/security
|
||||
|
||||
security set-key-partition-list -S apple-tool:,apple: -k "$MY_KEYCHAIN_PASSWORD" "$MY_KEYCHAIN"
|
||||
|
||||
if [ "$1" == "hockeyapp" ] || [ "$1" == "appcenter-experimental" ] || [ "$1" == "appcenter-experimental-2" ]; then
|
||||
if [ "$1" == "hockeyapp" ] || [ "$1" == "appcenter-experimental" ] || [ "$1" == "appcenter-experimental-2" ] || [ "$1" == "appstore-development" ]; then
|
||||
APP_CONFIGURATION="release_arm64"
|
||||
elif [ "$1" == "appstore" ]; then
|
||||
APP_CONFIGURATION="release_universal"
|
||||
|
||||
@ -20,7 +20,9 @@ swift_library(
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
|
||||
"//submodules/MeshAnimationCache:MeshAnimationCache"
|
||||
"//submodules/MeshAnimationCache:MeshAnimationCache",
|
||||
"//submodules/Utils/RangeSet:RangeSet",
|
||||
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -11,6 +11,7 @@ import Display
|
||||
import DeviceLocationManager
|
||||
import TemporaryCachedPeerDataManager
|
||||
import MeshAnimationCache
|
||||
import InAppPurchaseManager
|
||||
|
||||
public final class TelegramApplicationOpenUrlCompletion {
|
||||
public let completion: (Bool) -> Void
|
||||
@ -28,9 +29,15 @@ public enum AccessType {
|
||||
case unreachable
|
||||
}
|
||||
|
||||
public enum TelegramAppBuildType {
|
||||
case `internal`
|
||||
case `public`
|
||||
}
|
||||
|
||||
public final class TelegramApplicationBindings {
|
||||
public let isMainApp: Bool
|
||||
public let appBundleId: String
|
||||
public let appBuildType: TelegramAppBuildType
|
||||
public let containerPath: String
|
||||
public let appSpecificScheme: String
|
||||
public let openUrl: (String) -> Void
|
||||
@ -55,9 +62,10 @@ public final class TelegramApplicationBindings {
|
||||
public let requestSetAlternateIconName: (String?, @escaping (Bool) -> Void) -> Void
|
||||
public let forceOrientation: (UIInterfaceOrientation) -> Void
|
||||
|
||||
public init(isMainApp: Bool, appBundleId: String, containerPath: String, appSpecificScheme: String, openUrl: @escaping (String) -> Void, openUniversalUrl: @escaping (String, TelegramApplicationOpenUrlCompletion) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal<Bool, NoError>, applicationIsActive: Signal<Bool, NoError>, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable, openSettings: @escaping () -> Void, openAppStorePage: @escaping () -> Void, registerForNotifications: @escaping (@escaping (Bool) -> Void) -> Void, requestSiriAuthorization: @escaping (@escaping (Bool) -> Void) -> Void, siriAuthorization: @escaping () -> AccessType, getWindowHost: @escaping () -> WindowHost?, presentNativeController: @escaping (UIViewController) -> Void, dismissNativeController: @escaping () -> Void, getAvailableAlternateIcons: @escaping () -> [PresentationAppIcon], getAlternateIconName: @escaping () -> String?, requestSetAlternateIconName: @escaping (String?, @escaping (Bool) -> Void) -> Void, forceOrientation: @escaping (UIInterfaceOrientation) -> Void) {
|
||||
public init(isMainApp: Bool, appBundleId: String, appBuildType: TelegramAppBuildType, containerPath: String, appSpecificScheme: String, openUrl: @escaping (String) -> Void, openUniversalUrl: @escaping (String, TelegramApplicationOpenUrlCompletion) -> Void, canOpenUrl: @escaping (String) -> Bool, getTopWindow: @escaping () -> UIWindow?, displayNotification: @escaping (String) -> Void, applicationInForeground: Signal<Bool, NoError>, applicationIsActive: Signal<Bool, NoError>, clearMessageNotifications: @escaping ([MessageId]) -> Void, pushIdleTimerExtension: @escaping () -> Disposable, openSettings: @escaping () -> Void, openAppStorePage: @escaping () -> Void, registerForNotifications: @escaping (@escaping (Bool) -> Void) -> Void, requestSiriAuthorization: @escaping (@escaping (Bool) -> Void) -> Void, siriAuthorization: @escaping () -> AccessType, getWindowHost: @escaping () -> WindowHost?, presentNativeController: @escaping (UIViewController) -> Void, dismissNativeController: @escaping () -> Void, getAvailableAlternateIcons: @escaping () -> [PresentationAppIcon], getAlternateIconName: @escaping () -> String?, requestSetAlternateIconName: @escaping (String?, @escaping (Bool) -> Void) -> Void, forceOrientation: @escaping (UIInterfaceOrientation) -> Void) {
|
||||
self.isMainApp = isMainApp
|
||||
self.appBundleId = appBundleId
|
||||
self.appBuildType = appBuildType
|
||||
self.containerPath = containerPath
|
||||
self.appSpecificScheme = appSpecificScheme
|
||||
self.openUrl = openUrl
|
||||
@ -168,6 +176,23 @@ public enum ResolvedUrlSettingsSection {
|
||||
case devices
|
||||
}
|
||||
|
||||
public struct ResolvedBotChoosePeerTypes: OptionSet {
|
||||
public var rawValue: UInt32
|
||||
|
||||
public init(rawValue: UInt32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.rawValue = 0
|
||||
}
|
||||
|
||||
public static let users = ResolvedBotChoosePeerTypes(rawValue: 1)
|
||||
public static let bots = ResolvedBotChoosePeerTypes(rawValue: 2)
|
||||
public static let groups = ResolvedBotChoosePeerTypes(rawValue: 4)
|
||||
public static let channels = ResolvedBotChoosePeerTypes(rawValue: 16)
|
||||
}
|
||||
|
||||
public struct ResolvedBotAdminRights: OptionSet {
|
||||
public var rawValue: UInt32
|
||||
|
||||
@ -252,13 +277,12 @@ public enum ResolvedUrl {
|
||||
case share(url: String?, text: String?, to: String?)
|
||||
case wallpaper(WallpaperUrlParameter)
|
||||
case theme(String)
|
||||
#if ENABLE_WALLET
|
||||
case wallet(address: String, amount: Int64?, comment: String?)
|
||||
#endif
|
||||
case settings(ResolvedUrlSettingsSection)
|
||||
case joinVoiceChat(PeerId, String?)
|
||||
case importStickers
|
||||
case startAttach(peerId: PeerId, payload: String?)
|
||||
case startAttach(peerId: PeerId, payload: String?, choose: ResolvedBotChoosePeerTypes?)
|
||||
case invoice(slug: String, invoice: TelegramMediaInvoice)
|
||||
case premiumOffer(reference: String?)
|
||||
}
|
||||
|
||||
public enum NavigateToChatKeepStack {
|
||||
@ -656,6 +680,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
var locationManager: DeviceLocationManager? { get }
|
||||
var callManager: PresentationCallManager? { get }
|
||||
var contactDataManager: DeviceContactDataManager? { get }
|
||||
var inAppPurchaseManager: InAppPurchaseManager? { get }
|
||||
|
||||
var activeAccountContexts: Signal<(primary: AccountContext?, accounts: [(AccountRecordId, AccountContext, Int32)], currentAuth: UnauthorizedAccount?), NoError> { get }
|
||||
var activeAccountsWithInfo: Signal<(primary: AccountRecordId?, accounts: [AccountWithInfo]), NoError> { get }
|
||||
@ -695,8 +720,8 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func openStorageUsage(context: AccountContext)
|
||||
func openLocationScreen(context: AccountContext, messageId: MessageId, navigationController: NavigationController)
|
||||
func openExternalUrl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void)
|
||||
func chatAvailableMessageActions(postbox: Postbox, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>) -> Signal<ChatAvailableMessageActions, NoError>
|
||||
func chatAvailableMessageActions(postbox: Postbox, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>, messages: [EngineMessage.Id: EngineMessage], peers: [EnginePeer.Id: EnginePeer]) -> Signal<ChatAvailableMessageActions, NoError>
|
||||
func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>) -> Signal<ChatAvailableMessageActions, NoError>
|
||||
func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set<EngineMessage.Id>, messages: [EngineMessage.Id: EngineMessage], peers: [EnginePeer.Id: EnginePeer]) -> Signal<ChatAvailableMessageActions, NoError>
|
||||
func resolveUrl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal<ResolvedUrl, NoError>
|
||||
func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?)
|
||||
func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void)
|
||||
@ -711,6 +736,8 @@ public protocol SharedAccountContext: AnyObject {
|
||||
|
||||
func makeChatQrCodeScreen(context: AccountContext, peer: Peer) -> ViewController
|
||||
|
||||
func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource) -> ViewController
|
||||
|
||||
func navigateToCurrentCall()
|
||||
var hasOngoingCall: ValuePromise<Bool> { get }
|
||||
var immediateHasOngoingCall: Bool { get }
|
||||
@ -722,6 +749,26 @@ public protocol SharedAccountContext: AnyObject {
|
||||
func beginNewAuth(testingEnvironment: Bool)
|
||||
}
|
||||
|
||||
public enum PremiumIntroSource {
|
||||
case settings
|
||||
case stickers
|
||||
case reactions
|
||||
case ads
|
||||
case upload
|
||||
case groupsAndChannels
|
||||
case pinnedChats
|
||||
case publicLinks
|
||||
case savedGifs
|
||||
case savedStickers
|
||||
case folders
|
||||
case chatsPerFolder
|
||||
case accounts
|
||||
case appIcons
|
||||
case about
|
||||
case deeplink(String?)
|
||||
case profile(PeerId)
|
||||
}
|
||||
|
||||
#if ENABLE_WALLET
|
||||
private final class TonInstanceData {
|
||||
var config: String?
|
||||
@ -822,6 +869,10 @@ public protocol AccountContext: AnyObject {
|
||||
var cachedGroupCallContexts: AccountGroupCallContextCache { get }
|
||||
var meshAnimationCache: MeshAnimationCache { get }
|
||||
|
||||
var animatedEmojiStickers: [String: [StickerPackItem]] { get }
|
||||
|
||||
var userLimits: EngineConfiguration.UserLimits { get }
|
||||
|
||||
func storeSecureIdPassword(password: String)
|
||||
func getStoredSecureIdPassword() -> String?
|
||||
|
||||
|
||||
@ -28,8 +28,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
public let isCopyProtectionEnabled: Bool
|
||||
public let availableReactions: AvailableReactions?
|
||||
public let defaultReaction: String?
|
||||
public let isPremium: Bool
|
||||
|
||||
public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set<EnginePeer.Id> = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: String?) {
|
||||
public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set<EnginePeer.Id> = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: String?, isPremium: Bool) {
|
||||
self.automaticDownloadPeerType = automaticDownloadPeerType
|
||||
self.automaticDownloadNetworkType = automaticDownloadNetworkType
|
||||
self.isRecentActions = isRecentActions
|
||||
@ -43,6 +44,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
self.isCopyProtectionEnabled = isCopyProtectionEnabled
|
||||
self.availableReactions = availableReactions
|
||||
self.defaultReaction = defaultReaction
|
||||
self.isPremium = isPremium
|
||||
}
|
||||
|
||||
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
|
||||
@ -82,6 +84,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
|
||||
if lhs.availableReactions != rhs.availableReactions {
|
||||
return false
|
||||
}
|
||||
if lhs.isPremium != rhs.isPremium {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import Foundation
|
||||
import UIKit
|
||||
import Postbox
|
||||
import Display
|
||||
import TelegramCore
|
||||
|
||||
public protocol ChatListController: ViewController {
|
||||
var context: AccountContext { get }
|
||||
@ -11,5 +12,5 @@ public protocol ChatListController: ViewController {
|
||||
func activateSearch(filter: ChatListSearchFilter, query: String?)
|
||||
func deactivateSearch(animated: Bool)
|
||||
func activateCompose()
|
||||
func maybeAskForPeerChatRemoval(peer: RenderedPeer, joined: Bool, deleteGloballyIfPossible: Bool, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void)
|
||||
func maybeAskForPeerChatRemoval(peer: EngineRenderedPeer, joined: Bool, deleteGloballyIfPossible: Bool, completion: @escaping (Bool) -> Void, removed: @escaping () -> Void)
|
||||
}
|
||||
|
||||
@ -56,8 +56,9 @@ public final class ContactMultiselectionControllerParams {
|
||||
public let filters: [ContactListFilter]
|
||||
public let alwaysEnabled: Bool
|
||||
public let limit: Int32?
|
||||
public let reachedLimit: ((Int32) -> Void)?
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter] = [.excludeSelf], alwaysEnabled: Bool = false, limit: Int32? = nil) {
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: ContactMultiselectionControllerMode, options: [ContactListAdditionalOption], filters: [ContactListFilter] = [.excludeSelf], alwaysEnabled: Bool = false, limit: Int32? = nil, reachedLimit: ((Int32) -> Void)? = nil) {
|
||||
self.context = context
|
||||
self.updatedPresentationData = updatedPresentationData
|
||||
self.mode = mode
|
||||
@ -65,6 +66,7 @@ public final class ContactMultiselectionControllerParams {
|
||||
self.filters = filters
|
||||
self.alwaysEnabled = alwaysEnabled
|
||||
self.limit = limit
|
||||
self.reachedLimit = reachedLimit
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -148,8 +148,6 @@ public enum FetchManagerPriority: Comparable {
|
||||
}
|
||||
}
|
||||
|
||||
public let FetchCompleteRange = IndexSet(integersIn: 0 ..< Int(Int32.max) as Range<Int>)
|
||||
|
||||
public protocol FetchManager {
|
||||
var queue: Queue { get }
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ public func freeMediaFileInteractiveFetched(account: Account, fileReference: Fil
|
||||
public func freeMediaFileInteractiveFetched(fetchManager: FetchManager, fileReference: FileMediaReference, priority: FetchManagerPriority) -> Signal<Void, NoError> {
|
||||
let file = fileReference.media
|
||||
let mediaReference = AnyMediaReference.standalone(media: fileReference.media)
|
||||
return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(PeerId(0)), locationKey: .free, mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: IndexSet(integersIn: 0 ..< Int(Int32.max) as Range<Int>), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil)
|
||||
return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(PeerId(0)), locationKey: .free, mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: IndexSet(integersIn: 0 ..< Int(Int64.max) as Range<Int>), statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: false, priority: priority, storeToDownloadsPeerType: nil)
|
||||
}
|
||||
|
||||
public func freeMediaFileResourceInteractiveFetched(account: Account, fileReference: FileMediaReference, resource: MediaResource) -> Signal<FetchResourceSourceType, FetchResourceError> {
|
||||
@ -37,7 +37,7 @@ public func messageMediaFileInteractiveFetched(context: AccountContext, message:
|
||||
return messageMediaFileInteractiveFetched(fetchManager: context.fetchManager, messageId: message.id, messageReference: MessageReference(message), file: file, userInitiated: userInitiated, priority: .userInitiated)
|
||||
}
|
||||
|
||||
public func messageMediaFileInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, file: TelegramMediaFile, ranges: IndexSet = IndexSet(integersIn: 0 ..< Int(Int32.max) as Range<Int>), userInitiated: Bool, priority: FetchManagerPriority) -> Signal<Void, NoError> {
|
||||
public func messageMediaFileInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, file: TelegramMediaFile, ranges: IndexSet = IndexSet(integersIn: 0 ..< Int(Int64.max) as Range<Int>), userInitiated: Bool, priority: FetchManagerPriority) -> Signal<Void, NoError> {
|
||||
let mediaReference = AnyMediaReference.message(message: messageReference, media: file)
|
||||
return fetchManager.interactivelyFetched(category: fetchCategoryForFile(file), location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(file.resource), ranges: ranges, statsCategory: statsCategoryForFileWithAttributes(file.attributes), elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerType: nil)
|
||||
}
|
||||
@ -46,17 +46,17 @@ public func messageMediaFileCancelInteractiveFetch(context: AccountContext, mess
|
||||
context.fetchManager.cancelInteractiveFetches(category: fetchCategoryForFile(file), location: .chat(messageId.peerId), locationKey: .messageId(messageId), resource: file.resource)
|
||||
}
|
||||
|
||||
public func messageMediaImageInteractiveFetched(context: AccountContext, message: Message, image: TelegramMediaImage, resource: MediaResource, range: Range<Int>? = nil, userInitiated: Bool = true, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError> {
|
||||
public func messageMediaImageInteractiveFetched(context: AccountContext, message: Message, image: TelegramMediaImage, resource: MediaResource, range: Range<Int64>? = nil, userInitiated: Bool = true, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError> {
|
||||
return messageMediaImageInteractiveFetched(fetchManager: context.fetchManager, messageId: message.id, messageReference: MessageReference(message), image: image, resource: resource, range: range, userInitiated: userInitiated, priority: .userInitiated, storeToDownloadsPeerType: storeToDownloadsPeerType)
|
||||
}
|
||||
|
||||
public func messageMediaImageInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, image: TelegramMediaImage, resource: MediaResource, range: Range<Int>? = nil, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError> {
|
||||
public func messageMediaImageInteractiveFetched(fetchManager: FetchManager, messageId: MessageId, messageReference: MessageReference, image: TelegramMediaImage, resource: MediaResource, range: Range<Int64>? = nil, userInitiated: Bool, priority: FetchManagerPriority, storeToDownloadsPeerType: MediaAutoDownloadPeerType?) -> Signal<Void, NoError> {
|
||||
let mediaReference = AnyMediaReference.message(message: messageReference, media: image)
|
||||
let ranges: IndexSet
|
||||
if let range = range {
|
||||
ranges = IndexSet(integersIn: range)
|
||||
ranges = IndexSet(integersIn: Int(range.lowerBound) ..< Int(range.upperBound))
|
||||
} else {
|
||||
ranges = FetchCompleteRange
|
||||
ranges = IndexSet(integersIn: Int(0) ..< Int(Int64.max))
|
||||
}
|
||||
return fetchManager.interactivelyFetched(category: .image, location: .chat(messageId.peerId), locationKey: .messageId(messageId), mediaReference: mediaReference, resourceReference: mediaReference.resourceReference(resource), ranges: ranges, statsCategory: .image, elevatedPriority: false, userInitiated: userInitiated, priority: priority, storeToDownloadsPeerType: storeToDownloadsPeerType)
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import UIKit
|
||||
import AsyncDisplayKit
|
||||
import TelegramAudio
|
||||
import UniversalMediaPlayer
|
||||
import RangeSet
|
||||
|
||||
public enum PeerMessagesMediaPlaylistId: Equatable, SharedMediaPlaylistId {
|
||||
case peer(PeerId)
|
||||
@ -200,7 +201,7 @@ public protocol UniversalVideoManager: AnyObject {
|
||||
func addPlaybackCompleted(id: AnyHashable, _ f: @escaping () -> Void) -> Int
|
||||
func removePlaybackCompleted(id: AnyHashable, index: Int)
|
||||
func statusSignal(content: UniversalVideoContent) -> Signal<MediaPlayerStatus?, NoError>
|
||||
func bufferingStatusSignal(content: UniversalVideoContent) -> Signal<(IndexSet, Int)?, NoError>
|
||||
func bufferingStatusSignal(content: UniversalVideoContent) -> Signal<(RangeSet<Int64>, Int64)?, NoError>
|
||||
}
|
||||
|
||||
public enum AudioRecordingState: Equatable {
|
||||
|
||||
@ -30,6 +30,10 @@ public struct ChatListNodePeersFilter: OptionSet {
|
||||
|
||||
public static let excludeChannels = ChatListNodePeersFilter(rawValue: 1 << 12)
|
||||
public static let onlyGroupsAndChannels = ChatListNodePeersFilter(rawValue: 1 << 13)
|
||||
|
||||
public static let excludeGroups = ChatListNodePeersFilter(rawValue: 1 << 14)
|
||||
public static let excludeUsers = ChatListNodePeersFilter(rawValue: 1 << 15)
|
||||
public static let excludeBots = ChatListNodePeersFilter(rawValue: 1 << 16)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -8,11 +8,12 @@ import Display
|
||||
import TelegramAudio
|
||||
import UniversalMediaPlayer
|
||||
import AVFoundation
|
||||
import RangeSet
|
||||
|
||||
public protocol UniversalVideoContentNode: AnyObject {
|
||||
var ready: Signal<Void, NoError> { get }
|
||||
var status: Signal<MediaPlayerStatus, NoError> { get }
|
||||
var bufferingStatus: Signal<(IndexSet, Int)?, NoError> { get }
|
||||
var bufferingStatus: Signal<(RangeSet<Int64>, Int64)?, NoError> { get }
|
||||
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition)
|
||||
|
||||
@ -63,10 +64,11 @@ public protocol UniversalVideoDecoration: AnyObject {
|
||||
}
|
||||
|
||||
public enum UniversalVideoPriority: Int32, Comparable {
|
||||
case secondaryOverlay = 0
|
||||
case embedded = 1
|
||||
case gallery = 2
|
||||
case overlay = 3
|
||||
case minimal = 0
|
||||
case secondaryOverlay = 1
|
||||
case embedded = 2
|
||||
case gallery = 3
|
||||
case overlay = 4
|
||||
|
||||
public static func <(lhs: UniversalVideoPriority, rhs: UniversalVideoPriority) -> Bool {
|
||||
return lhs.rawValue < rhs.rawValue
|
||||
@ -104,8 +106,8 @@ public final class UniversalVideoNode: ASDisplayNode {
|
||||
return self._status.get()
|
||||
}
|
||||
|
||||
private let _bufferingStatus = Promise<(IndexSet, Int)?>()
|
||||
public var bufferingStatus: Signal<(IndexSet, Int)?, NoError> {
|
||||
private let _bufferingStatus = Promise<(RangeSet<Int64>, Int64)?>()
|
||||
public var bufferingStatus: Signal<(RangeSet<Int64>, Int64)?, NoError> {
|
||||
return self._bufferingStatus.get()
|
||||
}
|
||||
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
import TelegramCore
|
||||
|
||||
public struct VideoCallsConfiguration: Equatable {
|
||||
public enum VideoCallsSupport {
|
||||
case disabled
|
||||
case full
|
||||
case onlyVideo
|
||||
}
|
||||
|
||||
public var videoCallsSupport: VideoCallsSupport
|
||||
|
||||
public 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
|
||||
}
|
||||
}
|
||||
|
||||
public extension VideoCallsConfiguration {
|
||||
var areVideoCallsEnabled: Bool {
|
||||
switch self.videoCallsSupport {
|
||||
case .disabled:
|
||||
return false
|
||||
case .full, .onlyVideo:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -562,23 +562,23 @@ private final class AnimatedStickerDirectFrameSourceCache {
|
||||
}
|
||||
|
||||
|
||||
final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
public final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
private let queue: Queue
|
||||
private let data: Data
|
||||
private let width: Int
|
||||
private let height: Int
|
||||
private let cache: AnimatedStickerDirectFrameSourceCache?
|
||||
private let bytesPerRow: Int
|
||||
let frameCount: Int
|
||||
let frameRate: Int
|
||||
public let frameCount: Int
|
||||
public let frameRate: Int
|
||||
fileprivate var currentFrame: Int
|
||||
private let animation: LottieInstance
|
||||
|
||||
var frameIndex: Int {
|
||||
public var frameIndex: Int {
|
||||
return self.currentFrame % self.frameCount
|
||||
}
|
||||
|
||||
init?(queue: Queue, data: Data, width: Int, height: Int, cachePathPrefix: String?, useMetalCache: Bool = false, fitzModifier: EmojiFitzModifier?) {
|
||||
public init?(queue: Queue, data: Data, width: Int, height: Int, cachePathPrefix: String?, useMetalCache: Bool = false, fitzModifier: EmojiFitzModifier?) {
|
||||
self.queue = queue
|
||||
self.data = data
|
||||
self.width = width
|
||||
@ -604,7 +604,7 @@ final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
assert(self.queue.isCurrent())
|
||||
}
|
||||
|
||||
func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
||||
public func takeFrame(draw: Bool) -> AnimatedStickerFrame? {
|
||||
let frameIndex = self.currentFrame % self.frameCount
|
||||
self.currentFrame += 1
|
||||
if draw {
|
||||
@ -630,11 +630,11 @@ final class AnimatedStickerDirectFrameSource: AnimatedStickerFrameSource {
|
||||
}
|
||||
}
|
||||
|
||||
func skipToEnd() {
|
||||
public func skipToEnd() {
|
||||
self.currentFrame = self.frameCount - 1
|
||||
}
|
||||
|
||||
func skipToFrameIndex(_ index: Int) {
|
||||
public func skipToFrameIndex(_ index: Int) {
|
||||
self.currentFrame = index
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@ public protocol AnimatedStickerNodeSource {
|
||||
var isVideo: Bool { get }
|
||||
|
||||
func cachedDataPath(width: Int, height: Int) -> Signal<(String, Bool), NoError>
|
||||
func directDataPath() -> Signal<String, NoError>
|
||||
func directDataPath(attemptSynchronously: Bool) -> Signal<String?, NoError>
|
||||
}
|
||||
|
||||
public final class AnimatedStickerNode: ASDisplayNode {
|
||||
@ -163,6 +163,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
public var completed: (Bool) -> Void = { _ in }
|
||||
public var frameUpdated: (Int, Int) -> Void = { _, _ in }
|
||||
public private(set) var currentFrameIndex: Int = 0
|
||||
public private(set) var currentFrameCount: Int = 0
|
||||
private var playFromIndex: Int?
|
||||
|
||||
private let timer = Atomic<SwiftSignalKit.Timer?>(value: nil)
|
||||
@ -303,9 +304,10 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
strongSelf.play(firstFrame: true)
|
||||
}
|
||||
}
|
||||
self.disposable.set((source.directDataPath()
|
||||
self.disposable.set((source.directDataPath(attemptSynchronously: false)
|
||||
|> filter { $0 != nil }
|
||||
|> deliverOnMainQueue).start(next: { path in
|
||||
f(path)
|
||||
f(path!)
|
||||
}))
|
||||
case .cached:
|
||||
self.disposable.set((source.cachedDataPath(width: width, height: height)
|
||||
@ -373,6 +375,11 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
private var isSetUpForPlayback = false
|
||||
|
||||
public func playOnce() {
|
||||
self.playbackMode = .once
|
||||
self.play()
|
||||
}
|
||||
|
||||
public func play(firstFrame: Bool = false, fromIndex: Int? = nil) {
|
||||
if !firstFrame {
|
||||
@ -452,6 +459,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
|
||||
strongSelf.frameUpdated(frame.index, frame.totalFrames)
|
||||
strongSelf.currentFrameIndex = frame.index
|
||||
strongSelf.currentFrameCount = frame.totalFrames
|
||||
|
||||
if frame.isLastFrame {
|
||||
var stopped = false
|
||||
@ -556,6 +564,7 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
||||
|
||||
strongSelf.frameUpdated(frame.index, frame.totalFrames)
|
||||
strongSelf.currentFrameIndex = frame.index
|
||||
strongSelf.currentFrameCount = frame.totalFrames;
|
||||
|
||||
if frame.isLastFrame {
|
||||
var stopped = false
|
||||
|
||||
@ -212,14 +212,12 @@ using AS::MutexLocker;
|
||||
displayBlock = ^id{
|
||||
CHECK_CANCELLED_AND_RETURN_NIL();
|
||||
|
||||
ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
|
||||
|
||||
for (dispatch_block_t block in displayBlocks) {
|
||||
CHECK_CANCELLED_AND_RETURN_NIL(ASGraphicsEndImageContext());
|
||||
block();
|
||||
}
|
||||
|
||||
UIImage *image = ASGraphicsGetImageAndEndCurrentContext();
|
||||
UIImage *image = ASGraphicsCreateImage(self.primitiveTraitCollection, bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, ^{
|
||||
for (dispatch_block_t block in displayBlocks) {
|
||||
if (isCancelledBlock()) return;
|
||||
block();
|
||||
}
|
||||
});
|
||||
|
||||
ASDN_DELAY_FOR_DISPLAY();
|
||||
return image;
|
||||
@ -228,38 +226,35 @@ using AS::MutexLocker;
|
||||
displayBlock = ^id{
|
||||
CHECK_CANCELLED_AND_RETURN_NIL();
|
||||
|
||||
if (shouldCreateGraphicsContext) {
|
||||
ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay);
|
||||
CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); );
|
||||
}
|
||||
|
||||
CGContextRef currentContext = UIGraphicsGetCurrentContext();
|
||||
UIImage *image = nil;
|
||||
|
||||
if (shouldCreateGraphicsContext && !currentContext) {
|
||||
//ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size));
|
||||
return nil;
|
||||
}
|
||||
|
||||
// For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or
|
||||
// _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs.
|
||||
[self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters];
|
||||
|
||||
if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly.
|
||||
image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock];
|
||||
} else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext.
|
||||
[self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
|
||||
}
|
||||
|
||||
[self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor];
|
||||
|
||||
if (shouldCreateGraphicsContext) {
|
||||
CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); );
|
||||
image = ASGraphicsGetImageAndEndCurrentContext();
|
||||
}
|
||||
|
||||
ASDN_DELAY_FOR_DISPLAY();
|
||||
return image;
|
||||
__block UIImage *image = nil;
|
||||
void (^workWithContext)() = ^{
|
||||
CGContextRef currentContext = UIGraphicsGetCurrentContext();
|
||||
|
||||
if (shouldCreateGraphicsContext && !currentContext) {
|
||||
ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size));
|
||||
return;
|
||||
}
|
||||
|
||||
// For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or
|
||||
// _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs.
|
||||
[self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters];
|
||||
|
||||
if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly.
|
||||
image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock];
|
||||
} else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext.
|
||||
[self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing];
|
||||
}
|
||||
|
||||
[self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor];
|
||||
ASDN_DELAY_FOR_DISPLAY();
|
||||
};
|
||||
|
||||
if (shouldCreateGraphicsContext) {
|
||||
return ASGraphicsCreateImage(self.primitiveTraitCollection, bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, workWithContext);
|
||||
} else {
|
||||
workWithContext();
|
||||
return image;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -309,9 +304,6 @@ using AS::MutexLocker;
|
||||
}
|
||||
|
||||
__instanceLock__.lock();
|
||||
ASCornerRoundingType cornerRoundingType = _cornerRoundingType;
|
||||
CGFloat cornerRadius = _cornerRadius;
|
||||
CGFloat contentsScale = _contentsScaleForDisplay;
|
||||
ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext;
|
||||
__instanceLock__.unlock();
|
||||
|
||||
@ -320,48 +312,6 @@ using AS::MutexLocker;
|
||||
didDisplayNodeContentWithRenderingContext(context, drawParameters);
|
||||
}
|
||||
}
|
||||
|
||||
if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0f) {
|
||||
CGRect bounds = CGRectZero;
|
||||
if (context == NULL) {
|
||||
bounds = self.threadSafeBounds;
|
||||
bounds.size.width *= contentsScale;
|
||||
bounds.size.height *= contentsScale;
|
||||
CGFloat white = 0.0f, alpha = 0.0f;
|
||||
[backgroundColor getWhite:&white alpha:&alpha];
|
||||
ASGraphicsBeginImageContextWithOptions(bounds.size, (alpha == 1.0f), contentsScale);
|
||||
[*image drawInRect:bounds];
|
||||
} else {
|
||||
bounds = CGContextGetClipBoundingBox(context);
|
||||
}
|
||||
|
||||
ASDisplayNodeAssert(UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self);
|
||||
|
||||
UIBezierPath *roundedHole = [UIBezierPath bezierPathWithRect:bounds];
|
||||
[roundedHole appendPath:[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius * contentsScale]];
|
||||
roundedHole.usesEvenOddFillRule = YES;
|
||||
|
||||
UIBezierPath *roundedPath = nil;
|
||||
if (borderWidth > 0.0f) { // Don't create roundedPath and stroke if borderWidth is 0.0
|
||||
CGFloat strokeThickness = borderWidth * contentsScale;
|
||||
CGFloat strokeInset = ((strokeThickness + 1.0f) / 2.0f) - 1.0f;
|
||||
roundedPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(bounds, strokeInset, strokeInset)
|
||||
cornerRadius:_cornerRadius * contentsScale];
|
||||
roundedPath.lineWidth = strokeThickness;
|
||||
[[UIColor colorWithCGColor:borderColor] setStroke];
|
||||
}
|
||||
|
||||
// Punch out the corners by copying the backgroundColor over them.
|
||||
// This works for everything from clearColor to opaque colors.
|
||||
[backgroundColor setFill];
|
||||
[roundedHole fillWithBlendMode:kCGBlendModeCopy alpha:1.0f];
|
||||
|
||||
[roundedPath stroke]; // Won't do anything if borderWidth is 0 and roundedPath is nil.
|
||||
|
||||
if (*image) {
|
||||
*image = ASGraphicsGetImageAndEndCurrentContext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously
|
||||
|
||||
@ -1539,41 +1539,6 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
ASPerformBlockOnMainThread(^{
|
||||
for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) {
|
||||
// Layers are, in order: Top Left, Top Right, Bottom Right, Bottom Left.
|
||||
// anchorPoint is Bottom Left at 0,0 and Top Right at 1,1.
|
||||
BOOL isTop = (idx == 0 || idx == 1);
|
||||
BOOL isRight = (idx == 1 || idx == 2);
|
||||
|
||||
CGSize size = CGSizeMake(radius + 1, radius + 1);
|
||||
ASGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay);
|
||||
|
||||
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
||||
if (isRight == YES) {
|
||||
CGContextTranslateCTM(ctx, -radius + 1, 0);
|
||||
}
|
||||
if (isTop == YES) {
|
||||
CGContextTranslateCTM(ctx, 0, -radius + 1);
|
||||
}
|
||||
UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius * 2, radius * 2) cornerRadius:radius];
|
||||
[roundedRect setUsesEvenOddFillRule:YES];
|
||||
[roundedRect appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(-1, -1, radius * 2 + 1, radius * 2 + 1)]];
|
||||
[backgroundColor setFill];
|
||||
[roundedRect fill];
|
||||
|
||||
// No lock needed, as _clipCornerLayers is only modified on the main thread.
|
||||
CALayer *clipCornerLayer = _clipCornerLayers[idx];
|
||||
clipCornerLayer.contents = (id)(ASGraphicsGetImageAndEndCurrentContext().CGImage);
|
||||
clipCornerLayer.bounds = CGRectMake(0.0, 0.0, size.width, size.height);
|
||||
clipCornerLayer.anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 1.0 : 0.0);
|
||||
}
|
||||
[self _layoutClipCornersIfNeeded];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)_setClipCornerLayersVisible:(BOOL)visible
|
||||
{
|
||||
}
|
||||
@ -1634,7 +1599,6 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
|
||||
}
|
||||
else if (newRoundingType == ASCornerRoundingTypeClipping) {
|
||||
// Clip corners already exist, but the radius has changed.
|
||||
[self _updateClipCornerLayerContentsWithRadius:newCornerRadius backgroundColor:self.backgroundColor];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,161 +7,130 @@
|
||||
//
|
||||
|
||||
#import <AsyncDisplayKit/ASGraphicsContext.h>
|
||||
#import <AsyncDisplayKit/ASCGImageBuffer.h>
|
||||
#import <AsyncDisplayKit/ASAssert.h>
|
||||
#import <AsyncDisplayKit/ASConfigurationInternal.h>
|
||||
#import <AsyncDisplayKit/ASInternalHelpers.h>
|
||||
#import <UIKit/UIGraphics.h>
|
||||
#import <UIKit/UIImage.h>
|
||||
#import <objc/runtime.h>
|
||||
#import <AsyncDisplayKit/ASAvailability.h>
|
||||
|
||||
/**
|
||||
* Our version of the private CGBitmapGetAlignedBytesPerRow function.
|
||||
*
|
||||
* In both 32-bit and 64-bit, this function rounds up to nearest multiple of 32
|
||||
* in iOS 9, 10, and 11. We'll try to catch if this ever changes by asserting that
|
||||
* the bytes-per-row for a 1x1 context from the system is 32.
|
||||
*/
|
||||
static size_t ASGraphicsGetAlignedBytesPerRow(size_t baseValue) {
|
||||
// Add 31 then zero out low 5 bits.
|
||||
return (baseValue + 31) & ~0x1F;
|
||||
}
|
||||
|
||||
/**
|
||||
* A key used to associate CGContextRef -> NSMutableData, nonatomic retain.
|
||||
*
|
||||
* That way the data will be released when the context dies. If they pull an image,
|
||||
* we will retain the data object (in a CGDataProvider) before releasing the context.
|
||||
*/
|
||||
static UInt8 __contextDataAssociationKey;
|
||||
#if AS_AT_LEAST_IOS13
|
||||
#define ASPerformBlockWithTraitCollection(work, traitCollection) \
|
||||
if (@available(iOS 13.0, tvOS 13.0, *)) { \
|
||||
UITraitCollection *uiTraitCollection = ASPrimitiveTraitCollectionToUITraitCollection(traitCollection); \
|
||||
[uiTraitCollection performAsCurrentTraitCollection:^{ \
|
||||
work(); \
|
||||
}];\
|
||||
} else { \
|
||||
work(); \
|
||||
}
|
||||
#else
|
||||
#define ASPerformBlockWithTraitCollection(work, traitCollection) work();
|
||||
#endif
|
||||
|
||||
#pragma mark - Graphics Contexts
|
||||
|
||||
void ASGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)
|
||||
NS_AVAILABLE_IOS(10)
|
||||
NS_INLINE void ASConfigureExtendedRange(UIGraphicsImageRendererFormat *format)
|
||||
{
|
||||
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
|
||||
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
|
||||
return;
|
||||
if (AS_AVAILABLE_IOS_TVOS(12, 12)) {
|
||||
// nop. We always use automatic range on iOS >= 12.
|
||||
} else {
|
||||
// Currently we never do wide color. One day we could pipe this information through from the ASImageNode if it was worth it.
|
||||
format.prefersExtendedRange = NO;
|
||||
}
|
||||
|
||||
// We use "reference contexts" to get device-specific options that UIKit
|
||||
// uses.
|
||||
static dispatch_once_t onceToken;
|
||||
static CGContextRef refCtxOpaque;
|
||||
static CGContextRef refCtxTransparent;
|
||||
dispatch_once(&onceToken, ^{
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 1);
|
||||
refCtxOpaque = CGContextRetain(UIGraphicsGetCurrentContext());
|
||||
ASDisplayNodeCAssert(CGBitmapContextGetBytesPerRow(refCtxOpaque) == 32, @"Expected bytes per row to be aligned to 32. Has CGBitmapGetAlignedBytesPerRow implementation changed?");
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
// Make transparent ref context.
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1);
|
||||
refCtxTransparent = CGContextRetain(UIGraphicsGetCurrentContext());
|
||||
UIGraphicsEndImageContext();
|
||||
});
|
||||
|
||||
// These options are taken from UIGraphicsBeginImageContext.
|
||||
CGContextRef refCtx = opaque ? refCtxOpaque : refCtxTransparent;
|
||||
CGBitmapInfo bitmapInfo = CGBitmapContextGetBitmapInfo(refCtx);
|
||||
|
||||
if (scale == 0) {
|
||||
scale = ASScreenScale();
|
||||
}
|
||||
size_t intWidth = (size_t)ceil(size.width * scale);
|
||||
size_t intHeight = (size_t)ceil(size.height * scale);
|
||||
size_t bitsPerComponent = CGBitmapContextGetBitsPerComponent(refCtx);
|
||||
size_t bytesPerRow = CGBitmapContextGetBitsPerPixel(refCtx) * intWidth / 8;
|
||||
bytesPerRow = ASGraphicsGetAlignedBytesPerRow(bytesPerRow);
|
||||
size_t bufferSize = bytesPerRow * intHeight;
|
||||
CGColorSpaceRef colorspace = CGBitmapContextGetColorSpace(refCtx);
|
||||
|
||||
// We create our own buffer, and wrap the context around that. This way we can prevent
|
||||
// the copy that usually gets made when you form a CGImage from the context.
|
||||
ASCGImageBuffer *buffer = [[ASCGImageBuffer alloc] initWithLength:bufferSize];
|
||||
|
||||
CGContextRef context = CGBitmapContextCreate(buffer.mutableBytes, intWidth, intHeight, bitsPerComponent, bytesPerRow, colorspace, bitmapInfo);
|
||||
|
||||
// Transfer ownership of the data to the context. So that if the context
|
||||
// is destroyed before we create an image from it, the data will be released.
|
||||
objc_setAssociatedObject((__bridge id)context, &__contextDataAssociationKey, buffer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
|
||||
// Set the CTM to account for iOS orientation & specified scale.
|
||||
// If only we could use CGContextSetBaseCTM. It doesn't
|
||||
// seem like there are any consequences for our use case
|
||||
// but we'll be on the look out. The internet hinted that it
|
||||
// affects shadowing but I tested and shadowing works.
|
||||
CGContextTranslateCTM(context, 0, intHeight);
|
||||
CGContextScaleCTM(context, scale, -scale);
|
||||
|
||||
// Save the state so we can restore it and recover our scale in GetImageAndEnd
|
||||
CGContextSaveGState(context);
|
||||
|
||||
// Transfer context ownership to the UIKit stack.
|
||||
UIGraphicsPushContext(context);
|
||||
CGContextRelease(context);
|
||||
}
|
||||
|
||||
UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext() NS_RETURNS_RETAINED
|
||||
UIImage *ASGraphicsCreateImageWithOptions(CGSize size, BOOL opaque, CGFloat scale, UIImage *sourceImage,
|
||||
asdisplaynode_iscancelled_block_t NS_NOESCAPE isCancelled,
|
||||
void (^NS_NOESCAPE work)())
|
||||
{
|
||||
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
|
||||
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
return image;
|
||||
}
|
||||
|
||||
// Pop the context and make sure we have one.
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
if (context == NULL) {
|
||||
ASDisplayNodeCFailAssert(@"Can't end image context without having begun one.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Read the device-specific ICC-based color space to use for the image.
|
||||
// For DeviceRGB contexts (e.g. UIGraphics), CGBitmapContextCreateImage
|
||||
// generates an image in a device-specific color space (for wide color support).
|
||||
// We replicate that behavior, even though at this time CA does not
|
||||
// require the image to be in this space. Plain DeviceRGB images seem
|
||||
// to be treated exactly the same, but better safe than sorry.
|
||||
static CGColorSpaceRef imageColorSpace;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0);
|
||||
UIImage *refImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
imageColorSpace = CGColorSpaceRetain(CGImageGetColorSpace(refImage.CGImage));
|
||||
ASDisplayNodeCAssertNotNil(imageColorSpace, nil);
|
||||
UIGraphicsEndImageContext();
|
||||
});
|
||||
|
||||
// Retrieve our buffer and create a CGDataProvider from it.
|
||||
ASCGImageBuffer *buffer = objc_getAssociatedObject((__bridge id)context, &__contextDataAssociationKey);
|
||||
ASDisplayNodeCAssertNotNil(buffer, nil);
|
||||
CGDataProviderRef provider = [buffer createDataProviderAndInvalidate];
|
||||
|
||||
// Create the CGImage. Options taken from CGBitmapContextCreateImage.
|
||||
CGImageRef cgImg = CGImageCreate(CGBitmapContextGetWidth(context), CGBitmapContextGetHeight(context), CGBitmapContextGetBitsPerComponent(context), CGBitmapContextGetBitsPerPixel(context), CGBitmapContextGetBytesPerRow(context), imageColorSpace, CGBitmapContextGetBitmapInfo(context), provider, NULL, true, kCGRenderingIntentDefault);
|
||||
CGDataProviderRelease(provider);
|
||||
|
||||
// We saved our GState right after setting the CTM so that we could restore it
|
||||
// here and get the original scale back.
|
||||
CGContextRestoreGState(context);
|
||||
CGFloat scale = CGContextGetCTM(context).a;
|
||||
|
||||
// Note: popping from the UIKit stack will probably destroy the context.
|
||||
context = NULL;
|
||||
UIGraphicsPopContext();
|
||||
|
||||
UIImage *result = [[UIImage alloc] initWithCGImage:cgImg scale:scale orientation:UIImageOrientationUp];
|
||||
CGImageRelease(cgImg);
|
||||
return result;
|
||||
return ASGraphicsCreateImage(ASPrimitiveTraitCollectionMakeDefault(), size, opaque, scale, sourceImage, isCancelled, work);
|
||||
}
|
||||
|
||||
void ASGraphicsEndImageContext()
|
||||
{
|
||||
if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) {
|
||||
UIGraphicsEndImageContext();
|
||||
return;
|
||||
UIImage *ASGraphicsCreateImage(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * sourceImage, asdisplaynode_iscancelled_block_t NS_NOESCAPE isCancelled, void (NS_NOESCAPE ^work)()) {
|
||||
if (@available(iOS 10.0, *)) {
|
||||
if (true /*ASActivateExperimentalFeature(ASExperimentalDrawingGlobal)*/) {
|
||||
// If they used default scale, reuse one of two preferred formats.
|
||||
static UIGraphicsImageRendererFormat *defaultFormat;
|
||||
static UIGraphicsImageRendererFormat *opaqueFormat;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
|
||||
defaultFormat = [UIGraphicsImageRendererFormat preferredFormat];
|
||||
opaqueFormat = [UIGraphicsImageRendererFormat preferredFormat];
|
||||
} else {
|
||||
defaultFormat = [UIGraphicsImageRendererFormat defaultFormat];
|
||||
opaqueFormat = [UIGraphicsImageRendererFormat defaultFormat];
|
||||
}
|
||||
opaqueFormat.opaque = YES;
|
||||
ASConfigureExtendedRange(defaultFormat);
|
||||
ASConfigureExtendedRange(opaqueFormat);
|
||||
});
|
||||
|
||||
UIGraphicsImageRendererFormat *format;
|
||||
if (sourceImage) {
|
||||
if (sourceImage.renderingMode == UIImageRenderingModeAlwaysTemplate) {
|
||||
// Template images will be black and transparent, so if we use
|
||||
// sourceImage.imageRenderFormat it will assume a grayscale color space.
|
||||
// This is not good because a template image should be able to tint to any color,
|
||||
// so we'll just use the default here.
|
||||
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
|
||||
format = [UIGraphicsImageRendererFormat preferredFormat];
|
||||
} else {
|
||||
format = [UIGraphicsImageRendererFormat defaultFormat];
|
||||
}
|
||||
} else {
|
||||
format = sourceImage.imageRendererFormat;
|
||||
}
|
||||
// We only want the private bits (color space and bits per component) from the image.
|
||||
// We have our own ideas about opacity and scale.
|
||||
format.opaque = opaque;
|
||||
format.scale = scale;
|
||||
} else if (scale == 0 || scale == ASScreenScale()) {
|
||||
format = opaque ? opaqueFormat : defaultFormat;
|
||||
} else {
|
||||
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
|
||||
format = [UIGraphicsImageRendererFormat preferredFormat];
|
||||
} else {
|
||||
format = [UIGraphicsImageRendererFormat defaultFormat];
|
||||
}
|
||||
if (opaque) format.opaque = YES;
|
||||
format.scale = scale;
|
||||
ASConfigureExtendedRange(format);
|
||||
}
|
||||
|
||||
// Avoid using the imageWithActions: method because it does not support cancellation at the
|
||||
// last moment i.e. before actually creating the resulting image.
|
||||
__block UIImage *image;
|
||||
NSError *error;
|
||||
[[[UIGraphicsImageRenderer alloc] initWithSize:size format:format]
|
||||
runDrawingActions:^(UIGraphicsImageRendererContext *rendererContext) {
|
||||
ASDisplayNodeCAssert(UIGraphicsGetCurrentContext(), @"Should have a context!");
|
||||
ASPerformBlockWithTraitCollection(work, traitCollection);
|
||||
}
|
||||
completionActions:^(UIGraphicsImageRendererContext *rendererContext) {
|
||||
if (isCancelled == nil || !isCancelled()) {
|
||||
image = rendererContext.currentImage;
|
||||
}
|
||||
}
|
||||
error:&error];
|
||||
if (error) {
|
||||
NSCAssert(NO, @"Error drawing: %@", error);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
UIGraphicsPopContext();
|
||||
|
||||
// Bad OS or experiment flag. Use UIGraphics* API.
|
||||
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
|
||||
ASPerformBlockWithTraitCollection(work, traitCollection)
|
||||
UIImage *image = nil;
|
||||
if (isCancelled == nil || !isCancelled()) {
|
||||
image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
}
|
||||
UIGraphicsEndImageContext();
|
||||
return image;
|
||||
}
|
||||
|
||||
UIImage *ASGraphicsCreateImageWithTraitCollectionAndOptions(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * sourceImage, void (NS_NOESCAPE ^work)()) {
|
||||
return ASGraphicsCreateImage(traitCollection, size, opaque, scale, sourceImage, nil, work);
|
||||
}
|
||||
|
||||
@ -8,11 +8,52 @@
|
||||
//
|
||||
|
||||
#import <AsyncDisplayKit/ASTextKitComponents.h>
|
||||
#import "ASTextKitContext.h"
|
||||
#import <AsyncDisplayKit/ASAssert.h>
|
||||
#import <AsyncDisplayKit/ASMainThreadDeallocation.h>
|
||||
|
||||
#import <tgmath.h>
|
||||
|
||||
@implementation ASCustomTextContainer
|
||||
|
||||
- (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(nullable CGRect *)remainingRect {
|
||||
CGRect result = [super lineFragmentRectForProposedRect:proposedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect];
|
||||
|
||||
#if DEBUG
|
||||
if (result.origin.y < 10.0f) {
|
||||
result.size.width -= 21.0f;
|
||||
if (result.size.width < 0.0f) {
|
||||
result.size.width = 0.0f;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface ASCustomLayoutManager : NSLayoutManager
|
||||
|
||||
@end
|
||||
|
||||
@implementation ASCustomLayoutManager
|
||||
|
||||
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {
|
||||
/*CGGlyph glyph = [self glyphAtIndex:glyphsToShow.location];
|
||||
if (glyph) {
|
||||
}
|
||||
|
||||
CGRect bounds = [self boundingRectForGlyphRange:glyphsToShow inTextContainer:[self textContainerForGlyphAtIndex:glyphsToShow.location effectiveRange:nil]];
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSetFillColorWithColor(context, [UIColor grayColor].CGColor);
|
||||
CGContextFillRect(context, bounds);*/
|
||||
|
||||
[super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface ASTextKitComponentsTextView () {
|
||||
// Prevent UITextView from updating contentOffset while deallocating: https://github.com/TextureGroup/Texture/issues/860
|
||||
BOOL _deallocating;
|
||||
@ -83,7 +124,7 @@
|
||||
|
||||
return [self componentsWithTextStorage:textStorage
|
||||
textContainerSize:textContainerSize
|
||||
layoutManager:[[NSLayoutManager alloc] init]];
|
||||
layoutManager:[[ASCustomLayoutManager alloc] init]];
|
||||
}
|
||||
|
||||
+ (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage
|
||||
@ -97,7 +138,8 @@
|
||||
components.layoutManager = layoutManager;
|
||||
[components.textStorage addLayoutManager:components.layoutManager];
|
||||
|
||||
components.textContainer = [[NSTextContainer alloc] initWithSize:textContainerSize];
|
||||
components.textContainer = [[ASCustomTextContainer alloc] initWithSize:textContainerSize];
|
||||
//components.textContainer.exclusionPaths = @[[UIBezierPath bezierPathWithRect:CGRectMake(textContainerSize.width - 60.0, 0.0, 60.0, 40.0)]];
|
||||
components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view.
|
||||
[components.layoutManager addTextContainer:components.textContainer];
|
||||
|
||||
|
||||
@ -50,4 +50,8 @@ AS_SUBCLASSING_RESTRICTED
|
||||
|
||||
@end
|
||||
|
||||
@interface ASCustomTextContainer : NSTextContainer
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
[_textStorage setAttributedString:attributedString];
|
||||
}
|
||||
|
||||
_textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize];
|
||||
_textContainer = [[ASCustomTextContainer alloc] initWithSize:constrainedSize];
|
||||
// We want the text laid out up to the very edges of the container.
|
||||
_textContainer.lineFragmentPadding = 0;
|
||||
_textContainer.lineBreakMode = lineBreakMode;
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define AS_TLS_AVAILABLE 0
|
||||
#define AS_TLS_AVAILABLE 1
|
||||
|
||||
#ifndef AS_ENABLE_TEXTNODE
|
||||
#define AS_ENABLE_TEXTNODE 1 // Enable old TextNode by default
|
||||
|
||||
@ -6,46 +6,60 @@
|
||||
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <AsyncDisplayKit/ASBaseDefines.h>
|
||||
#import <CoreGraphics/CoreGraphics.h>
|
||||
|
||||
@class UIImage;
|
||||
|
||||
/**
|
||||
* Functions for creating one-shot graphics contexts that do not have to copy
|
||||
* their contents when an image is generated from them. This is efficient
|
||||
* for our use, since we do not reuse graphics contexts.
|
||||
*
|
||||
* The API mirrors the UIGraphics API, with the exception that forming an image
|
||||
* ends the context as well.
|
||||
*
|
||||
* Note: You must not mix-and-match between ASGraphics* and UIGraphics* functions
|
||||
* within the same drawing operation.
|
||||
*/
|
||||
#import <AsyncDisplayKit/ASBlockTypes.h>
|
||||
#import <AsyncDisplayKit/ASTraitCollection.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* Creates a one-shot context.
|
||||
* A wrapper for the UIKit drawing APIs. If you are in ASExperimentalDrawingGlobal, and you have iOS >= 10, we will create
|
||||
* a UIGraphicsRenderer with an appropriate format. Otherwise, we will use UIGraphicsBeginImageContext et al.
|
||||
*
|
||||
* Behavior is the same as UIGraphicsBeginImageContextWithOptions.
|
||||
* @param size The size of the context.
|
||||
* @param opaque Whether the context should be opaque or not.
|
||||
* @param scale The scale of the context. 0 uses main screen scale.
|
||||
* @param sourceImage If you are planning to render a UIImage into this context, provide it here and we will use its
|
||||
* preferred renderer format if we are using UIGraphicsImageRenderer.
|
||||
* @param isCancelled An optional block for canceling the drawing before forming the image. Only takes effect under
|
||||
* the legacy code path, as UIGraphicsRenderer does not support cancellation.
|
||||
* @param work A block, wherein the current UIGraphics context is set based on the arguments.
|
||||
*
|
||||
* @return The rendered image. You can also render intermediary images using UIGraphicsGetImageFromCurrentImageContext.
|
||||
*/
|
||||
AS_EXTERN void ASGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale);
|
||||
UIImage *ASGraphicsCreateImageWithOptions(CGSize size, BOOL opaque, CGFloat scale, UIImage * _Nullable sourceImage, asdisplaynode_iscancelled_block_t NS_NOESCAPE _Nullable isCancelled, void (NS_NOESCAPE ^work)(void)) ASDISPLAYNODE_DEPRECATED_MSG("Use ASGraphicsCreateImageWithTraitCollectionAndOptions instead");
|
||||
|
||||
/**
|
||||
* Generates and image and ends the current one-shot context.
|
||||
*
|
||||
* Behavior is the same as UIGraphicsGetImageFromCurrentImageContext followed by UIGraphicsEndImageContext.
|
||||
*/
|
||||
AS_EXTERN UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext(void) NS_RETURNS_RETAINED;
|
||||
* A wrapper for the UIKit drawing APIs. If you are in ASExperimentalDrawingGlobal, and you have iOS >= 10, we will create
|
||||
* a UIGraphicsRenderer with an appropriate format. Otherwise, we will use UIGraphicsBeginImageContext et al.
|
||||
*
|
||||
* @param traitCollection Trait collection. The `work` block will be executed with this trait collection, so it will affect dynamic colors, etc.
|
||||
* @param size The size of the context.
|
||||
* @param opaque Whether the context should be opaque or not.
|
||||
* @param scale The scale of the context. 0 uses main screen scale.
|
||||
* @param sourceImage If you are planning to render a UIImage into this context, provide it here and we will use its
|
||||
* preferred renderer format if we are using UIGraphicsImageRenderer.
|
||||
* @param isCancelled An optional block for canceling the drawing before forming the image.
|
||||
* @param work A block, wherein the current UIGraphics context is set based on the arguments.
|
||||
*
|
||||
* @return The rendered image. You can also render intermediary images using UIGraphicsGetImageFromCurrentImageContext.
|
||||
*/
|
||||
UIImage *ASGraphicsCreateImage(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * _Nullable sourceImage, asdisplaynode_iscancelled_block_t _Nullable NS_NOESCAPE isCancelled, void (NS_NOESCAPE ^work)(void));
|
||||
|
||||
/**
|
||||
* Call this if you want to end the current context without making an image.
|
||||
*
|
||||
* Behavior is the same as UIGraphicsEndImageContext.
|
||||
*/
|
||||
AS_EXTERN void ASGraphicsEndImageContext(void);
|
||||
* A wrapper for the UIKit drawing APIs.
|
||||
*
|
||||
* @param traitCollection Trait collection. The `work` block will be executed with this trait collection, so it will affect dynamic colors, etc.
|
||||
* @param size The size of the context.
|
||||
* @param opaque Whether the context should be opaque or not.
|
||||
* @param scale The scale of the context. 0 uses main screen scale.
|
||||
* @param sourceImage If you are planning to render a UIImage into this context, provide it here and we will use its
|
||||
* preferred renderer format if we are using UIGraphicsImageRenderer.
|
||||
* @param work A block, wherein the current UIGraphics context is set based on the arguments.
|
||||
*
|
||||
* @return The rendered image. You can also render intermediary images using UIGraphicsGetImageFromCurrentImageContext.
|
||||
*/
|
||||
UIImage *ASGraphicsCreateImageWithTraitCollectionAndOptions(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * _Nullable sourceImage, void (NS_NOESCAPE ^work)(void)) ASDISPLAYNODE_DEPRECATED_MSG("Use ASGraphicsCreateImage instead");
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@ -212,6 +212,7 @@ namespace AS {
|
||||
if (AS_AVAILABLE_IOS_TVOS(10, 10)) {
|
||||
gMutex_unfair = ASActivateExperimentalFeature(ASExperimentalUnfairLock);
|
||||
}
|
||||
gMutex_unfair = true;
|
||||
});
|
||||
|
||||
if (recursive) {
|
||||
|
||||
@ -28,6 +28,7 @@ swift_library(
|
||||
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
|
||||
"//submodules/Pasteboard:Pasteboard",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -18,6 +18,7 @@ import InvisibleInkDustNode
|
||||
import TextInputMenu
|
||||
import ChatPresentationInterfaceState
|
||||
import Pasteboard
|
||||
import EmojiTextAttachmentView
|
||||
|
||||
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
|
||||
private let minInputFontSize: CGFloat = 5.0
|
||||
@ -208,7 +209,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
|
||||
baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
||||
}
|
||||
textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed)
|
||||
textInputNode.attributedText = textAttributedStringForStateText(state.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count)
|
||||
self.updatingInputState = false
|
||||
self.updateTextNodeText(animated: animated)
|
||||
@ -241,6 +242,10 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
private var spoilersRevealed = false
|
||||
|
||||
private var emojiViewProvider: ((String) -> UIView)?
|
||||
|
||||
private var maxCaptionLength: Int32?
|
||||
|
||||
public init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, isCaption: Bool = false, isAttachment: Bool = false, presentController: @escaping (ViewController) -> Void) {
|
||||
self.context = context
|
||||
self.presentationInterfaceState = presentationInterfaceState
|
||||
@ -311,7 +316,32 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
}
|
||||
self.textInputBackgroundNode.view.addGestureRecognizer(recognizer)
|
||||
|
||||
/*self.emojiViewProvider = { [weak self] emoji in
|
||||
guard let strongSelf = self, let file = strongSelf.context.animatedEmojiStickers[emoji]?.first?.file else {
|
||||
return UIView()
|
||||
}
|
||||
|
||||
return EmojiTextAttachmentView(context: context, file: file)
|
||||
}*/
|
||||
|
||||
self.updateSendButtonEnabled(isCaption || isAttachment, animated: false)
|
||||
|
||||
if self.isCaption || self.isAttachment {
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> mapToSignal { peer -> Signal<Int32, NoError> in
|
||||
if let peer = peer {
|
||||
return self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.UserLimits.init(isPremium: peer.isPremium))
|
||||
|> map { limits in
|
||||
return limits.maxCaptionLength
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
|> deliverOnMainQueue).start(next: { [weak self] maxCaptionLength in
|
||||
self?.maxCaptionLength = maxCaptionLength
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public var sendPressed: ((NSAttributedString?) -> Void)?
|
||||
@ -322,7 +352,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
guard let presentationInterfaceState = self.presentationInterfaceState else {
|
||||
return 0.0
|
||||
}
|
||||
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact))
|
||||
return self.updateLayout(width: size.width, leftInset: sideInset, rightInset: sideInset, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: size.height, isSecondary: false, transition: .immediate, interfaceState: presentationInterfaceState, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact))
|
||||
}
|
||||
|
||||
public func setCaption(_ caption: NSAttributedString?) {
|
||||
@ -466,7 +496,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
return minimalHeight
|
||||
}
|
||||
|
||||
public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
|
||||
public func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, additionalSideInsets: UIEdgeInsets, maxHeight: CGFloat, isSecondary: Bool, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, metrics: LayoutMetrics) -> CGFloat {
|
||||
let hadLayout = self.validLayout != nil
|
||||
let previousAdditionalSideInsets = self.validLayout?.3
|
||||
self.validLayout = (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, isSecondary)
|
||||
@ -741,7 +771,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
@objc public func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) {
|
||||
if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState {
|
||||
let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
||||
refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed)
|
||||
refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize)
|
||||
|
||||
self.updateSpoiler()
|
||||
@ -876,9 +906,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
textInputNode.textView.isScrollEnabled = false
|
||||
|
||||
refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed)
|
||||
refreshChatTextInputAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
|
||||
textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed)
|
||||
textInputNode.attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
|
||||
if textInputNode.textView.subviews.count > 1, animated {
|
||||
let containerView = textInputNode.textView.subviews[1]
|
||||
@ -920,8 +950,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
private func updateCounterTextNode(transition: ContainedViewLayoutTransition) {
|
||||
let inputTextMaxLength: Int32?
|
||||
if self.isCaption || self.isAttachment {
|
||||
inputTextMaxLength = self.context.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength
|
||||
if let maxCaptionLength = self.maxCaptionLength {
|
||||
inputTextMaxLength = maxCaptionLength
|
||||
} else {
|
||||
inputTextMaxLength = nil
|
||||
}
|
||||
@ -968,7 +998,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
let textFont = Font.regular(baseFontSize)
|
||||
let accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
|
||||
|
||||
let attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false)
|
||||
let attributedText = textAttributedStringForStateText(self.inputTextState.inputText, fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: false, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
|
||||
let range = (attributedText.string as NSString).range(of: "\n")
|
||||
if range.location != NSNotFound {
|
||||
@ -1080,7 +1110,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
self.focusUpdated?(true)
|
||||
|
||||
if self.isCaption, let (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, isSecondary) = self.validLayout, let presentationInterfaceState = self.presentationInterfaceState {
|
||||
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .animated(duration: 0.3, curve: .easeInOut), interfaceState: presentationInterfaceState, metrics: metrics)
|
||||
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: 0.0, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .animated(duration: 0.3, curve: .easeInOut), interfaceState: presentationInterfaceState, metrics: metrics)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1091,7 +1121,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
self.focusUpdated?(false)
|
||||
|
||||
if self.isCaption, let (width, leftInset, rightInset, additionalSideInsets, maxHeight, metrics, isSecondary) = self.validLayout, let presentationInterfaceState = self.presentationInterfaceState {
|
||||
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .animated(duration: 0.3, curve: .easeInOut), interfaceState: presentationInterfaceState, metrics: metrics)
|
||||
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: 0.0, additionalSideInsets: additionalSideInsets, maxHeight: maxHeight, isSecondary: isSecondary, transition: .animated(duration: 0.3, curve: .easeInOut), interfaceState: presentationInterfaceState, metrics: metrics)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1234,7 +1264,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if cleanText != text {
|
||||
let string = NSMutableAttributedString(attributedString: editableTextNode.attributedText ?? NSAttributedString())
|
||||
var textColor: UIColor = .black
|
||||
@ -1245,7 +1275,7 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
accentTextColor = presentationInterfaceState.theme.chat.inputPanel.panelControlAccentColor
|
||||
baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize)
|
||||
}
|
||||
let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed)
|
||||
let cleanReplacementString = textAttributedStringForStateText(NSAttributedString(string: cleanText), fontSize: baseFontSize, textColor: textColor, accentTextColor: accentTextColor, writingDirection: nil, spoilersRevealed: self.spoilersRevealed, availableEmojis: Set(self.context.animatedEmojiStickers.keys), emojiViewProvider: self.emojiViewProvider)
|
||||
string.replaceCharacters(in: range, with: cleanReplacementString)
|
||||
self.textInputNode?.attributedText = string
|
||||
self.textInputNode?.selectedRange = NSMakeRange(range.lowerBound + cleanReplacementString.length, 0)
|
||||
@ -1290,8 +1320,8 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS
|
||||
|
||||
@objc func sendButtonPressed() {
|
||||
let inputTextMaxLength: Int32?
|
||||
if self.isCaption || self.isAttachment {
|
||||
inputTextMaxLength = self.context.currentLimitsConfiguration.with { $0 }.maxMediaCaptionLength
|
||||
if let maxCaptionLength = self.maxCaptionLength {
|
||||
inputTextMaxLength = maxCaptionLength
|
||||
} else {
|
||||
inputTextMaxLength = nil
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ swift_library(
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
"//submodules/MediaResources:MediaResources",
|
||||
"//submodules/SemanticStatusNode:SemanticStatusNode",
|
||||
"//submodules/MoreButtonNode:MoreButtonNode",
|
||||
"//submodules/Components/AnimatedStickerComponent:AnimatedStickerComponent",
|
||||
],
|
||||
visibility = [
|
||||
|
||||
@ -380,6 +380,8 @@ public class AttachmentController: ViewController {
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.disablesInteractiveModalDismiss = true
|
||||
|
||||
self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
||||
|
||||
if let controller = self.controller {
|
||||
@ -567,7 +569,7 @@ public class AttachmentController: ViewController {
|
||||
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0)
|
||||
|
||||
let targetPosition = self.container.position
|
||||
let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height)
|
||||
let startPosition = targetPosition.offsetBy(dx: 0.0, dy: layout.size.height)
|
||||
|
||||
self.container.position = startPosition
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
|
||||
@ -783,7 +785,9 @@ public class AttachmentController: ViewController {
|
||||
}
|
||||
|
||||
let controllers = self.currentControllers
|
||||
containerTransition.updateFrame(node: self.container, frame: CGRect(origin: CGPoint(), size: containerRect.size))
|
||||
if !self.animating {
|
||||
containerTransition.updateFrame(node: self.container, frame: CGRect(origin: CGPoint(), size: containerRect.size))
|
||||
}
|
||||
|
||||
let containerLayout = containerLayout.withUpdatedIntrinsicInsets(containerInsets)
|
||||
|
||||
@ -832,11 +836,7 @@ public class AttachmentController: ViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
print()
|
||||
}
|
||||
|
||||
|
||||
public required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@ -1096,7 +1096,7 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
if textInputPanelNode.frame.width.isZero {
|
||||
panelTransition = .immediate
|
||||
}
|
||||
let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics)
|
||||
let panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: insets.left + layout.safeInsets.left, rightInset: insets.right + layout.safeInsets.right, bottomInset: 0.0, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics)
|
||||
let panelFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: panelHeight)
|
||||
if textInputPanelNode.frame.width.isZero {
|
||||
textInputPanelNode.frame = panelFrame
|
||||
|
||||
@ -22,7 +22,6 @@ swift_library(
|
||||
"//submodules/AlertUI:AlertUI",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/AnimationUI:AnimationUI",
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
|
||||
@ -5,7 +5,6 @@ import AsyncDisplayKit
|
||||
import Display
|
||||
import SolidRoundedButtonNode
|
||||
import SwiftSignalKit
|
||||
import OverlayStatusController
|
||||
import AnimationUI
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
|
||||
@ -18,7 +18,9 @@ public func authorizationCurrentOptionText(_ type: SentAuthorizationCodeType, st
|
||||
let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: primaryColor)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(16.0), textColor: primaryColor)
|
||||
return parseMarkdownIntoAttributedString(strings.Login_ShortCallTitle, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil }), textAlignment: .center)
|
||||
case .call, .flashCall:
|
||||
case .call:
|
||||
return NSAttributedString(string: strings.Login_CodeSentCall, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center)
|
||||
case .flashCall:
|
||||
return NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center)
|
||||
}
|
||||
}
|
||||
@ -35,7 +37,13 @@ public func authorizationNextOptionText(currentType: SentAuthorizationCodeType,
|
||||
let timeString = NSString(format: "%d:%.02d", Int(minutes), Int(seconds))
|
||||
return (NSAttributedString(string: strings.Login_WillSendSms(timeString as String).string, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
|
||||
}
|
||||
case .call, .flashCall, .missedCall:
|
||||
case .call:
|
||||
if timeout <= 0 {
|
||||
return (NSAttributedString(string: strings.Login_CodeSentCall, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
|
||||
} else {
|
||||
return (NSAttributedString(string: String(format: strings.ChangePhoneNumberCode_CallTimer(String(format: "%d:%.2d", minutes, seconds)).string, minutes, seconds), font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
|
||||
}
|
||||
case .flashCall, .missedCall:
|
||||
if timeout <= 0 {
|
||||
return (NSAttributedString(string: strings.ChangePhoneNumberCode_Called, font: Font.regular(16.0), textColor: primaryColor, paragraphAlignment: .center), false)
|
||||
} else {
|
||||
|
||||
@ -55,21 +55,6 @@ private class AvatarNodeParameters: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func generateGradientFilledCircleImage(diameter: CGFloat, colors: NSArray) -> UIImage? {
|
||||
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
context.addEllipse(in: bounds)
|
||||
context.clip()
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: &locations)!
|
||||
|
||||
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions())
|
||||
})
|
||||
}
|
||||
|
||||
private let grayscaleColors: NSArray = [
|
||||
UIColor(rgb: 0xb1b1b1).cgColor, UIColor(rgb: 0xcdcdcd).cgColor
|
||||
]
|
||||
@ -117,7 +102,7 @@ public enum AvatarNodeImageOverride: Equatable {
|
||||
case savedMessagesIcon
|
||||
case repliesIcon
|
||||
case archivedChatsIcon(hiddenByDefault: Bool)
|
||||
case editAvatarIcon
|
||||
case editAvatarIcon(forceNone: Bool)
|
||||
case deletedIcon
|
||||
case phoneIcon
|
||||
}
|
||||
@ -322,8 +307,8 @@ public final class AvatarNode: ASDisplayNode {
|
||||
case let .archivedChatsIcon(hiddenByDefault):
|
||||
representation = nil
|
||||
icon = .archivedChatsIcon(hiddenByDefault: hiddenByDefault)
|
||||
case .editAvatarIcon:
|
||||
representation = peer?.smallProfileImage
|
||||
case let .editAvatarIcon(forceNone):
|
||||
representation = forceNone ? nil : peer?.smallProfileImage
|
||||
icon = .editAvatarIcon
|
||||
case .deletedIcon:
|
||||
representation = nil
|
||||
|
||||
@ -25,6 +25,9 @@ swift_library(
|
||||
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/CheckNode:CheckNode",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/Markdown:Markdown",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@ -6,8 +6,8 @@ import PassKit
|
||||
import ShimmerEffect
|
||||
|
||||
enum BotCheckoutActionButtonState: Equatable {
|
||||
case active(String)
|
||||
case applePay
|
||||
case active(text: String, isEnabled: Bool)
|
||||
case applePay(isEnabled: Bool)
|
||||
case placeholder
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
static var height: CGFloat = 52.0
|
||||
|
||||
private var activeFillColor: UIColor
|
||||
private var inactiveFillColor: UIColor
|
||||
private var foregroundColor: UIColor
|
||||
|
||||
private let activeBackgroundNode: ASImageNode
|
||||
@ -28,17 +29,23 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
|
||||
private var placeholderNode: ShimmerEffectNode?
|
||||
|
||||
init(activeFillColor: UIColor, foregroundColor: UIColor) {
|
||||
private var activeImage: UIImage?
|
||||
private var inactiveImage: UIImage?
|
||||
|
||||
init(activeFillColor: UIColor, inactiveFillColor: UIColor, foregroundColor: UIColor) {
|
||||
self.activeFillColor = activeFillColor
|
||||
self.inactiveFillColor = inactiveFillColor
|
||||
self.foregroundColor = foregroundColor
|
||||
|
||||
|
||||
let diameter: CGFloat = 20.0
|
||||
self.activeImage = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor)
|
||||
self.inactiveImage = generateStretchableFilledCircleImage(diameter: diameter, color: inactiveFillColor)
|
||||
|
||||
self.activeBackgroundNode = ASImageNode()
|
||||
self.activeBackgroundNode.displaysAsynchronously = false
|
||||
self.activeBackgroundNode.displayWithoutProcessing = true
|
||||
self.activeBackgroundNode.isLayerBacked = true
|
||||
self.activeBackgroundNode.image = generateStretchableFilledCircleImage(diameter: diameter, color: activeFillColor)
|
||||
self.activeBackgroundNode.image = self.activeImage
|
||||
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.displaysAsynchronously = false
|
||||
@ -75,7 +82,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
var labelSize = self.labelNode.bounds.size
|
||||
if let state = self.state {
|
||||
switch state {
|
||||
case let .active(title):
|
||||
case let .active(title, isEnabled):
|
||||
if let applePayButton = self.applePayButton {
|
||||
self.applePayButton = nil
|
||||
applePayButton.removeFromSuperview()
|
||||
@ -85,12 +92,20 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
self.placeholderNode = nil
|
||||
placeholderNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
let image = isEnabled ? self.activeImage : self.inactiveImage
|
||||
if let image = image, let currentImage = self.activeBackgroundNode.image, currentImage !== image {
|
||||
self.activeBackgroundNode.image = image
|
||||
self.activeBackgroundNode.layer.animate(from: currentImage.cgImage! as AnyObject, to: image.cgImage! as AnyObject, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.2)
|
||||
} else {
|
||||
self.activeBackgroundNode.image = image
|
||||
}
|
||||
|
||||
let makeLayout = TextNode.asyncLayout(self.labelNode)
|
||||
let (labelLayout, labelApply) = makeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: self.foregroundColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: size, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
let _ = labelApply()
|
||||
labelSize = labelLayout.size
|
||||
case .applePay:
|
||||
case let .applePay(isEnabled):
|
||||
if self.applePayButton == nil {
|
||||
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
||||
let applePayButton: PKPaymentButton
|
||||
@ -102,6 +117,7 @@ final class BotCheckoutActionButton: HighlightableButtonNode {
|
||||
applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside)
|
||||
self.view.addSubview(applePayButton)
|
||||
self.applePayButton = applePayButton
|
||||
applePayButton.isEnabled = isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,16 +15,19 @@ public final class BotCheckoutController: ViewController {
|
||||
|
||||
let form: BotPaymentForm
|
||||
let validatedFormInfo: BotPaymentValidatedFormInfo?
|
||||
let botPeer: EnginePeer?
|
||||
|
||||
private init(
|
||||
form: BotPaymentForm,
|
||||
validatedFormInfo: BotPaymentValidatedFormInfo?
|
||||
validatedFormInfo: BotPaymentValidatedFormInfo?,
|
||||
botPeer: EnginePeer?
|
||||
) {
|
||||
self.form = form
|
||||
self.validatedFormInfo = validatedFormInfo
|
||||
self.botPeer = botPeer
|
||||
}
|
||||
|
||||
public static func fetch(context: AccountContext, messageId: EngineMessage.Id) -> Signal<InputData, FetchError> {
|
||||
public static func fetch(context: AccountContext, source: BotPaymentInvoiceSource) -> Signal<InputData, FetchError> {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let themeParams: [String: Any] = [
|
||||
"bg_color": Int32(bitPattern: presentationData.theme.list.plainBackgroundColor.argb),
|
||||
@ -34,33 +37,40 @@ public final class BotCheckoutController: ViewController {
|
||||
"button_text_color": Int32(bitPattern: presentationData.theme.list.itemCheckColors.foregroundColor.argb)
|
||||
]
|
||||
|
||||
return context.engine.payments.fetchBotPaymentForm(messageId: messageId, themeParams: themeParams)
|
||||
return context.engine.payments.fetchBotPaymentForm(source: source, themeParams: themeParams)
|
||||
|> mapError { _ -> FetchError in
|
||||
return .generic
|
||||
}
|
||||
|> mapToSignal { paymentForm -> Signal<InputData, FetchError> in
|
||||
if let current = paymentForm.savedInfo {
|
||||
return context.engine.payments.validateBotPaymentForm(saveInfo: true, messageId: messageId, formInfo: current)
|
||||
|> mapError { _ -> FetchError in
|
||||
return .generic
|
||||
}
|
||||
|> map { result -> InputData in
|
||||
return InputData(
|
||||
form: paymentForm,
|
||||
validatedFormInfo: result
|
||||
)
|
||||
}
|
||||
|> `catch` { _ -> Signal<InputData, FetchError> in
|
||||
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: paymentForm.paymentBotId))
|
||||
|> castError(FetchError.self)
|
||||
|> mapToSignal { botPeer -> Signal<InputData, FetchError> in
|
||||
if let current = paymentForm.savedInfo {
|
||||
return context.engine.payments.validateBotPaymentForm(saveInfo: true, source: source, formInfo: current)
|
||||
|> mapError { _ -> FetchError in
|
||||
return .generic
|
||||
}
|
||||
|> map { result -> InputData in
|
||||
return InputData(
|
||||
form: paymentForm,
|
||||
validatedFormInfo: result,
|
||||
botPeer: botPeer
|
||||
)
|
||||
}
|
||||
|> `catch` { _ -> Signal<InputData, FetchError> in
|
||||
return .single(InputData(
|
||||
form: paymentForm,
|
||||
validatedFormInfo: nil,
|
||||
botPeer: botPeer
|
||||
))
|
||||
}
|
||||
} else {
|
||||
return .single(InputData(
|
||||
form: paymentForm,
|
||||
validatedFormInfo: nil
|
||||
validatedFormInfo: nil,
|
||||
botPeer: botPeer
|
||||
))
|
||||
}
|
||||
} else {
|
||||
return .single(InputData(
|
||||
form: paymentForm,
|
||||
validatedFormInfo: nil
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,8 +87,11 @@ public final class BotCheckoutController: ViewController {
|
||||
|
||||
private let context: AccountContext
|
||||
private let invoice: TelegramMediaInvoice
|
||||
private let messageId: EngineMessage.Id
|
||||
private let source: BotPaymentInvoiceSource
|
||||
private let completed: (String, EngineMessage.Id?) -> Void
|
||||
private let pending: () -> Void
|
||||
private let cancelled: () -> Void
|
||||
private let failed: () -> Void
|
||||
|
||||
private var presentationData: PresentationData
|
||||
|
||||
@ -86,12 +99,15 @@ public final class BotCheckoutController: ViewController {
|
||||
|
||||
private let inputData: Promise<BotCheckoutController.InputData?>
|
||||
|
||||
public init(context: AccountContext, invoice: TelegramMediaInvoice, messageId: EngineMessage.Id, inputData: Promise<BotCheckoutController.InputData?>, completed: @escaping (String, EngineMessage.Id?) -> Void) {
|
||||
public init(context: AccountContext, invoice: TelegramMediaInvoice, source: BotPaymentInvoiceSource, inputData: Promise<BotCheckoutController.InputData?>, completed: @escaping (String, EngineMessage.Id?) -> Void, pending: @escaping () -> Void = {}, cancelled: @escaping () -> Void = {}, failed: @escaping () -> Void = {}) {
|
||||
self.context = context
|
||||
self.invoice = invoice
|
||||
self.messageId = messageId
|
||||
self.source = source
|
||||
self.inputData = inputData
|
||||
self.completed = completed
|
||||
self.pending = pending
|
||||
self.cancelled = cancelled
|
||||
self.failed = failed
|
||||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
@ -113,7 +129,7 @@ public final class BotCheckoutController: ViewController {
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
let displayNode = BotCheckoutControllerNode(controller: self, navigationBar: self.navigationBar!, context: self.context, invoice: self.invoice, messageId: self.messageId, inputData: self.inputData, present: { [weak self] c, a in
|
||||
let displayNode = BotCheckoutControllerNode(controller: self, navigationBar: self.navigationBar!, context: self.context, invoice: self.invoice, source: self.source, inputData: self.inputData, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root), with: a)
|
||||
}, dismissAnimated: { [weak self] in
|
||||
self?.dismiss()
|
||||
@ -122,6 +138,12 @@ public final class BotCheckoutController: ViewController {
|
||||
displayNode.dismiss = { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}
|
||||
displayNode.pending = { [weak self] in
|
||||
self?.pending()
|
||||
}
|
||||
displayNode.failed = { [weak self] in
|
||||
self?.failed()
|
||||
}
|
||||
|
||||
self.displayNode = displayNode
|
||||
super.displayNodeDidLoad()
|
||||
@ -146,6 +168,7 @@ public final class BotCheckoutController: ViewController {
|
||||
}
|
||||
|
||||
@objc private func cancelPressed() {
|
||||
self.cancelled()
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||