mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Wallet improvements
This commit is contained in:
parent
ee9815cff8
commit
c64c16b8ee
6
Makefile
6
Makefile
@ -255,6 +255,12 @@ build_verbose: check_env
|
||||
//:IntentsExtension#dwarf-and-dsym,iphoneos-arm64 \
|
||||
--verbose 8 ${BUCK_OPTIONS} ${BUCK_THREADS_OPTIONS} ${BUCK_DEBUG_OPTIONS}
|
||||
|
||||
deps: check_env
|
||||
$(BUCK) query "deps(//:AppPackage)" --dot \
|
||||
${BUCK_OPTIONS} ${BUCK_DEBUG_OPTIONS}
|
||||
|
||||
|
||||
|
||||
build_openssl: check_env
|
||||
$(BUCK) build \
|
||||
//submodules/openssl:openssl#iphoneos-arm64 \
|
||||
|
@ -236,11 +236,11 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
||||
|> take(1)
|
||||
|> mapToSignal { matchedContacts in
|
||||
return account
|
||||
|> introduceError(IntentContactsError.self)
|
||||
|> castError(IntentContactsError.self)
|
||||
|> mapToSignal { account -> Signal<[(String, TelegramUser)], IntentContactsError> in
|
||||
if let account = account {
|
||||
return matchingCloudContacts(postbox: account.postbox, contacts: matchedContacts)
|
||||
|> introduceError(IntentContactsError.self)
|
||||
|> castError(IntentContactsError.self)
|
||||
} else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
@ -278,11 +278,11 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
||||
let account = self.accountPromise.get()
|
||||
|
||||
let signal = account
|
||||
|> introduceError(IntentHandlingError.self)
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> mapToSignal { account -> Signal<INPerson?, IntentHandlingError> in
|
||||
if let account = account {
|
||||
return matchingCloudContact(postbox: account.postbox, peerId: PeerId(peerId))
|
||||
|> introduceError(IntentHandlingError.self)
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> map { user -> INPerson? in
|
||||
if let user = user {
|
||||
return personWithUser(stableId: "tg\(peerId)", user: user)
|
||||
@ -394,7 +394,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
||||
func handle(intent: INSearchForMessagesIntent, completion: @escaping (INSearchForMessagesIntentResponse) -> Void) {
|
||||
self.actionDisposable.set((self.accountPromise.get()
|
||||
|> take(1)
|
||||
|> introduceError(IntentHandlingError.self)
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> mapToSignal { account -> Signal<[INMessage], IntentHandlingError> in
|
||||
guard let account = account else {
|
||||
return .fail(.generic)
|
||||
@ -409,7 +409,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
||||
}
|
||||
|
||||
return (completion |> timeout(4.0, queue: Queue.mainQueue(), alternate: .single(Void())))
|
||||
|> introduceError(IntentHandlingError.self)
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> take(1)
|
||||
|> mapToSignal { _ -> Signal<[INMessage], IntentHandlingError> in
|
||||
let messages: Signal<[INMessage], NoError>
|
||||
@ -419,7 +419,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
||||
messages = unreadMessages(account: account)
|
||||
}
|
||||
return messages
|
||||
|> introduceError(IntentHandlingError.self)
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> afterDisposed {
|
||||
account.shouldBeServiceTaskMaster.set(.single(.never))
|
||||
}
|
||||
@ -484,7 +484,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
||||
|
||||
for (_, messageId) in maxMessageIdsToApply {
|
||||
signals.append(applyMaxReadIndexInteractively(postbox: account.postbox, stateManager: account.stateManager, index: MessageIndex(id: messageId, timestamp: 0))
|
||||
|> introduceError(IntentHandlingError.self))
|
||||
|> castError(IntentHandlingError.self))
|
||||
}
|
||||
|
||||
if signals.isEmpty {
|
||||
@ -576,7 +576,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
||||
func handle(intent: INSearchCallHistoryIntent, completion: @escaping (INSearchCallHistoryIntentResponse) -> Void) {
|
||||
self.actionDisposable.set((self.accountPromise.get()
|
||||
|> take(1)
|
||||
|> introduceError(IntentHandlingError.self)
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> mapToSignal { account -> Signal<[CallRecord], IntentHandlingError> in
|
||||
guard let account = account else {
|
||||
return .fail(.generic)
|
||||
@ -584,7 +584,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
||||
|
||||
account.shouldBeServiceTaskMaster.set(.single(.now))
|
||||
return missedCalls(account: account)
|
||||
|> introduceError(IntentHandlingError.self)
|
||||
|> castError(IntentHandlingError.self)
|
||||
|> afterDisposed {
|
||||
account.shouldBeServiceTaskMaster.set(.single(.never))
|
||||
}
|
||||
|
@ -4754,3 +4754,21 @@ Any member of this group will be able to see messages in the channel.";
|
||||
"Channel.AdminLog.CanDeleteMessagesOfOthers" = "Delete Messages of Others";
|
||||
|
||||
"ChatSearch.ResultsTooltip" = "Tap to view as a list.";
|
||||
|
||||
"Updated.JustNow" = "updated just now";
|
||||
"Updated.MinutesAgo_0" = "updated %@ minutes ago"; //three to ten
|
||||
"Updated.MinutesAgo_1" = "updated 1 minute ago"; //one
|
||||
"Updated.MinutesAgo_2" = "updated 2 minutes ago"; //two
|
||||
"Updated.MinutesAgo_3_10" = "updated %@ minutes ago"; //three to ten
|
||||
"Updated.MinutesAgo_many" = "updated %@ minutes ago"; // more than ten
|
||||
"Updated.MinutesAgo_any" = "updated %@ minutes ago"; // more than ten
|
||||
"Updated.HoursAgo_0" = "updated %@ hours ago";
|
||||
"Updated.HoursAgo_1" = "updated 1 hour ago";
|
||||
"Updated.HoursAgo_2" = "updated 2 hours ago";
|
||||
"Updated.HoursAgo_3_10" = "updated %@ hours ago";
|
||||
"Updated.HoursAgo_any" = "updated %@ hours ago";
|
||||
"Updated.HoursAgo_many" = "updated %@ hours ago";
|
||||
"Updated.HoursAgo_0" = "updated %@ hours ago";
|
||||
"Updated.YesterdayAt" = "updated yesterday at %@";
|
||||
"Updated.AtDate" = "updated %@";
|
||||
"Updated.TodayAt" = "updated today at %@";
|
@ -22,7 +22,8 @@
|
||||
+ (DeviceSpecificEncryptionParameters * _Nonnull)deviceSpecificEncryptionParameters:(NSString * _Nonnull)rootPath baseAppBundleId:(NSString * _Nonnull)baseAppBundleId;
|
||||
- (NSData * _Nullable)bundleDataWithAppToken:(NSData * _Nullable)appToken;
|
||||
|
||||
+ (void)encryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion;
|
||||
+ (void)decryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion;
|
||||
+ (void)getHardwareEncryptionAvailableWithBaseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion;
|
||||
+ (void)encryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable, NSData * _Nullable))completion;
|
||||
+ (void)decryptApplicationSecret:(NSData * _Nonnull)secret publicKey:(NSData * _Nonnull)publicKey baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion;
|
||||
|
||||
@end
|
||||
|
@ -16,6 +16,8 @@
|
||||
#import <MtProtoKitDynamic/MtProtoKitDynamic.h>
|
||||
#endif
|
||||
|
||||
static NSString *telegramApplicationSecretKey = @"telegramApplicationSecretKey_v3";
|
||||
|
||||
static uint32_t funcSwap32(uint32_t input)
|
||||
{
|
||||
return OSSwapBigToHostInt32(input);
|
||||
@ -238,7 +240,7 @@ API_AVAILABLE(ios(10))
|
||||
}
|
||||
|
||||
- (NSData * _Nullable)encrypt:(NSData * _Nonnull)data;
|
||||
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data;
|
||||
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data cancelled:(bool *)cancelled;
|
||||
|
||||
@end
|
||||
|
||||
@ -258,6 +260,11 @@ API_AVAILABLE(ios(10))
|
||||
CFRelease(_publicKey);
|
||||
}
|
||||
|
||||
- (NSData * _Nullable)getPublicKey {
|
||||
NSData *result = CFBridgingRelease(SecKeyCopyExternalRepresentation(_publicKey, nil));
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSData * _Nullable)encrypt:(NSData * _Nonnull)data {
|
||||
if (data.length % 16 != 0) {
|
||||
return nil;
|
||||
@ -274,12 +281,17 @@ API_AVAILABLE(ios(10))
|
||||
return cipherText;
|
||||
}
|
||||
|
||||
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data {
|
||||
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data cancelled:(bool *)cancelled {
|
||||
CFErrorRef error = NULL;
|
||||
NSData *plainText = (NSData *)CFBridgingRelease(SecKeyCreateDecryptedData(_privateKey, kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM, (__bridge CFDataRef)data, &error));
|
||||
|
||||
if (!plainText) {
|
||||
__unused NSError *err = CFBridgingRelease(error);
|
||||
if (err.code == -2) {
|
||||
if (cancelled) {
|
||||
*cancelled = true;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
@ -431,22 +443,29 @@ API_AVAILABLE(ios(10))
|
||||
return bundleSeedID;
|
||||
}
|
||||
|
||||
+ (LocalPrivateKey * _Nullable)getLocalPrivateKey:(NSString * _Nonnull)baseAppBundleId API_AVAILABLE(ios(10)) {
|
||||
+ (NSString * _Nonnull)applicationSecretTag:(bool)isCheckKey {
|
||||
if (isCheckKey) {
|
||||
return [[telegramApplicationSecretKey stringByAppendingString:@"_check"] dataUsingEncoding:NSUTF8StringEncoding];
|
||||
} else {
|
||||
return [telegramApplicationSecretKey dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
}
|
||||
|
||||
+ (LocalPrivateKey * _Nullable)getApplicationSecretKey:(NSString * _Nonnull)baseAppBundleId isCheckKey:(bool)isCheckKey API_AVAILABLE(ios(10)) {
|
||||
NSString *bundleSeedId = [self bundleSeedId];
|
||||
if (bundleSeedId == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *applicationTag = [self applicationSecretTag:isCheckKey];
|
||||
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
|
||||
|
||||
NSData *applicationTag = [@"telegramLocalKey" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSDictionary *query = @{
|
||||
(id)kSecClass: (id)kSecClassKey,
|
||||
(id)kSecAttrApplicationTag: applicationTag,
|
||||
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
|
||||
(id)kSecAttrAccessGroup: (id)accessGroup,
|
||||
(id)kSecReturnRef: @YES,
|
||||
(id)kSecReturnRef: @YES
|
||||
};
|
||||
SecKeyRef privateKey = NULL;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey);
|
||||
@ -474,13 +493,13 @@ API_AVAILABLE(ios(10))
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (bool)removeLocalPrivateKey:(NSString * _Nonnull)baseAppBundleId API_AVAILABLE(ios(10)) {
|
||||
+ (bool)removeApplicationSecretKey:(NSString * _Nonnull)baseAppBundleId isCheckKey:(bool)isCheckKey API_AVAILABLE(ios(10)) {
|
||||
NSString *bundleSeedId = [self bundleSeedId];
|
||||
if (bundleSeedId == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *applicationTag = [@"telegramLocalKey" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *applicationTag = [self applicationSecretTag:isCheckKey];
|
||||
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
|
||||
|
||||
NSDictionary *query = @{
|
||||
@ -496,142 +515,21 @@ API_AVAILABLE(ios(10))
|
||||
return true;
|
||||
}
|
||||
|
||||
+ (LocalPrivateKey * _Nullable)addLocalPrivateKey:(NSString * _Nonnull)baseAppBundleId API_AVAILABLE(ios(10)) {
|
||||
+ (LocalPrivateKey * _Nullable)addApplicationSecretKey:(NSString * _Nonnull)baseAppBundleId isCheckKey:(bool)isCheckKey API_AVAILABLE(ios(10)) {
|
||||
NSString *bundleSeedId = [self bundleSeedId];
|
||||
if (bundleSeedId == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *applicationTag = [@"telegramLocalKey" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *applicationTag = [self applicationSecretTag:isCheckKey];
|
||||
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
|
||||
|
||||
SecAccessControlRef access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleAlwaysThisDeviceOnly, kSecAccessControlPrivateKeyUsage, NULL);
|
||||
NSDictionary *attributes = @{
|
||||
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
|
||||
(id)kSecAttrKeySizeInBits: @256,
|
||||
(id)kSecAttrTokenID: (id)kSecAttrTokenIDSecureEnclave,
|
||||
(id)kSecPrivateKeyAttrs: @{
|
||||
(id)kSecAttrIsPermanent: @YES,
|
||||
(id)kSecAttrApplicationTag: applicationTag,
|
||||
(id)kSecAttrAccessControl: (__bridge id)access,
|
||||
(id)kSecAttrAccessGroup: (id)accessGroup,
|
||||
},
|
||||
};
|
||||
|
||||
CFErrorRef error = NULL;
|
||||
SecKeyRef privateKey = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &error);
|
||||
if (!privateKey) {
|
||||
if (access) {
|
||||
CFRelease(access);
|
||||
}
|
||||
|
||||
__unused NSError *err = CFBridgingRelease(error);
|
||||
return nil;
|
||||
SecAccessControlRef access;
|
||||
if (isCheckKey) {
|
||||
access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlPrivateKeyUsage, NULL);
|
||||
} else {
|
||||
access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlUserPresence | kSecAccessControlPrivateKeyUsage, NULL);
|
||||
}
|
||||
|
||||
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
|
||||
if (!publicKey) {
|
||||
if (privateKey) {
|
||||
CFRelease(privateKey);
|
||||
}
|
||||
if (access) {
|
||||
CFRelease(access);
|
||||
}
|
||||
|
||||
__unused NSError *err = CFBridgingRelease(error);
|
||||
return nil;
|
||||
}
|
||||
|
||||
LocalPrivateKey *result = [[LocalPrivateKey alloc] initWithPrivateKey:privateKey publicKey:publicKey];
|
||||
|
||||
if (publicKey) {
|
||||
CFRelease(publicKey);
|
||||
}
|
||||
if (privateKey) {
|
||||
CFRelease(privateKey);
|
||||
}
|
||||
if (access) {
|
||||
CFRelease(access);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (LocalPrivateKey * _Nullable)getApplicationSecretKey:(NSString * _Nonnull)baseAppBundleId API_AVAILABLE(ios(10)) {
|
||||
NSString *bundleSeedId = [self bundleSeedId];
|
||||
if (bundleSeedId == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
|
||||
|
||||
NSData *applicationTag = [@"telegramApplicationSecretKey" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSDictionary *query = @{
|
||||
(id)kSecClass: (id)kSecClassKey,
|
||||
(id)kSecAttrApplicationTag: applicationTag,
|
||||
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
|
||||
(id)kSecAttrAccessGroup: (id)accessGroup,
|
||||
(id)kSecReturnRef: @YES,
|
||||
};
|
||||
SecKeyRef privateKey = NULL;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey);
|
||||
if (status != errSecSuccess) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
|
||||
if (!publicKey) {
|
||||
if (privateKey) {
|
||||
CFRelease(privateKey);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
LocalPrivateKey *result = [[LocalPrivateKey alloc] initWithPrivateKey:privateKey publicKey:publicKey];
|
||||
|
||||
if (publicKey) {
|
||||
CFRelease(publicKey);
|
||||
}
|
||||
if (privateKey) {
|
||||
CFRelease(privateKey);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (bool)removeApplicationSecretKey:(NSString * _Nonnull)baseAppBundleId API_AVAILABLE(ios(10)) {
|
||||
NSString *bundleSeedId = [self bundleSeedId];
|
||||
if (bundleSeedId == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *applicationTag = [@"telegramApplicationSecretKey" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
|
||||
|
||||
NSDictionary *query = @{
|
||||
(id)kSecClass: (id)kSecClassKey,
|
||||
(id)kSecAttrApplicationTag: applicationTag,
|
||||
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
|
||||
(id)kSecAttrAccessGroup: (id)accessGroup
|
||||
};
|
||||
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
if (status != errSecSuccess) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+ (LocalPrivateKey * _Nullable)addApplicationSecretKey:(NSString * _Nonnull)baseAppBundleId API_AVAILABLE(ios(10)) {
|
||||
NSString *bundleSeedId = [self bundleSeedId];
|
||||
if (bundleSeedId == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *applicationTag = [@"telegramApplicationSecretKey" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
|
||||
|
||||
SecAccessControlRef access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleAlwaysThisDeviceOnly, kSecAccessControlUserPresence | kSecAccessControlPrivateKeyUsage, NULL);
|
||||
NSDictionary *attributes = @{
|
||||
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
|
||||
(id)kSecAttrKeySizeInBits: @256,
|
||||
@ -738,24 +636,67 @@ API_AVAILABLE(ios(10))
|
||||
return [[DeviceSpecificEncryptionParameters alloc] initWithKey:key salt:salt];
|
||||
}
|
||||
|
||||
+ (void)encryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
LocalPrivateKey *privateKey = [self getApplicationSecretKey:baseAppBundleId];
|
||||
if (privateKey == nil) {
|
||||
privateKey = [self addApplicationSecretKey:baseAppBundleId];
|
||||
+ (dispatch_queue_t)encryptionQueue {
|
||||
static dispatch_queue_t instance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = dispatch_queue_create("encryptionQueue", 0);
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
+ (void)getHardwareEncryptionAvailableWithBaseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion {
|
||||
dispatch_async([self encryptionQueue], ^{
|
||||
LocalPrivateKey *checkKey = [self getApplicationSecretKey:baseAppBundleId isCheckKey:true];
|
||||
if (checkKey != nil) {
|
||||
NSData *sampleData = [checkKey encrypt:[NSData data]];
|
||||
if (sampleData == nil) {
|
||||
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:false];
|
||||
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:true];
|
||||
} else {
|
||||
NSData *decryptedData = [checkKey decrypt:sampleData cancelled: nil];
|
||||
if (decryptedData == nil) {
|
||||
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:false];
|
||||
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:true];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:false];
|
||||
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:true];
|
||||
}
|
||||
|
||||
LocalPrivateKey *privateKey = [self getApplicationSecretKey:baseAppBundleId isCheckKey:false];
|
||||
if (privateKey == nil) {
|
||||
completion(nil);
|
||||
return;
|
||||
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:false];
|
||||
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:true];
|
||||
privateKey = [self addApplicationSecretKey:baseAppBundleId isCheckKey:false];
|
||||
privateKey = [self addApplicationSecretKey:baseAppBundleId isCheckKey:true];
|
||||
}
|
||||
NSData *result = [privateKey encrypt:secret];
|
||||
completion(result);
|
||||
completion([privateKey getPublicKey]);
|
||||
});
|
||||
}
|
||||
|
||||
+ (void)decryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
LocalPrivateKey *privateKey = [self getApplicationSecretKey:baseAppBundleId];
|
||||
+ (void)encryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable, NSData * _Nullable))completion {
|
||||
dispatch_async([self encryptionQueue], ^{
|
||||
LocalPrivateKey *privateKey = [self getApplicationSecretKey:baseAppBundleId isCheckKey:false];
|
||||
if (privateKey == nil) {
|
||||
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:false];
|
||||
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:true];
|
||||
privateKey = [self addApplicationSecretKey:baseAppBundleId isCheckKey:false];
|
||||
privateKey = [self addApplicationSecretKey:baseAppBundleId isCheckKey:true];
|
||||
}
|
||||
if (privateKey == nil) {
|
||||
completion(nil, nil);
|
||||
return;
|
||||
}
|
||||
NSData *result = [privateKey encrypt:secret];
|
||||
completion(result, [privateKey getPublicKey]);
|
||||
});
|
||||
}
|
||||
|
||||
+ (void)decryptApplicationSecret:(NSData * _Nonnull)secret publicKey:(NSData * _Nonnull)publicKey baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion {
|
||||
dispatch_async([self encryptionQueue], ^{
|
||||
LocalPrivateKey *privateKey = [self getApplicationSecretKey:baseAppBundleId isCheckKey:false];
|
||||
if (privateKey == nil) {
|
||||
completion(nil);
|
||||
return;
|
||||
@ -764,7 +705,16 @@ API_AVAILABLE(ios(10))
|
||||
completion(nil);
|
||||
return;
|
||||
}
|
||||
NSData *result = [privateKey decrypt:secret];
|
||||
NSData *currentPublicKey = [privateKey getPublicKey];
|
||||
if (currentPublicKey == nil) {
|
||||
completion(nil);
|
||||
return;
|
||||
}
|
||||
if (![publicKey isEqualToData:currentPublicKey]) {
|
||||
completion(nil);
|
||||
return;
|
||||
}
|
||||
NSData *result = [privateKey decrypt:secret cancelled:nil];
|
||||
completion(result);
|
||||
});
|
||||
}
|
||||
|
@ -242,6 +242,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
public final var visibleContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in }
|
||||
public final var visibleBottomContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in }
|
||||
public final var beganInteractiveDragging: () -> Void = { }
|
||||
public final var endedInteractiveDragging: () -> Void = { }
|
||||
public final var didEndScrolling: (() -> Void)?
|
||||
|
||||
private var currentGeneralScrollDirection: GeneralScrollDirection?
|
||||
@ -599,6 +600,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
||||
self.lastContentOffsetTimestamp = 0.0
|
||||
self.didEndScrolling?()
|
||||
}
|
||||
self.endedInteractiveDragging()
|
||||
}
|
||||
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
|
@ -86,7 +86,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
var statusBarStyle: StatusBarStyle = .Ignore
|
||||
var statusBarStyleUpdated: (() -> Void)?
|
||||
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
init(controllerRemoved: @escaping (ViewController) -> Void) {
|
||||
self.controllerRemoved = controllerRemoved
|
||||
@ -260,7 +260,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
} else {
|
||||
transitionType = .pop
|
||||
}
|
||||
self.state.pending = PendingChild(value: self.makeChild(layout: layout, value: last), transitionType: transitionType, transition: transition, update: { [weak self] pendingChild in
|
||||
self.state.pending = PendingChild(value: self.makeChild(layout: layout.withUpdatedInputHeight(nil), value: last), transitionType: transitionType, transition: transition, update: { [weak self] pendingChild in
|
||||
self?.pendingChildIsReady(pendingChild)
|
||||
})
|
||||
}
|
||||
@ -268,12 +268,16 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
var statusBarTransition = transition
|
||||
|
||||
if let pending = self.state.pending {
|
||||
if pending.isReady {
|
||||
self.state.pending = nil
|
||||
let previous = self.state.top
|
||||
previous?.value.view.endEditing(true)
|
||||
self.state.top = pending.value
|
||||
self.topTransition(from: previous, to: pending.value, transitionType: pending.transitionType, layout: layout, transition: pending.transition)
|
||||
self.topTransition(from: previous, to: pending.value, transitionType: pending.transitionType, layout: layout.withUpdatedInputHeight(nil), transition: pending.transition)
|
||||
statusBarTransition = pending.transition
|
||||
if !self.isReady {
|
||||
self.isReady = true
|
||||
self.isReadyUpdated?()
|
||||
@ -287,11 +291,16 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
self.topTransition(from: previous, to: nil, transitionType: .pop, layout: layout, transition: .immediate)
|
||||
}
|
||||
|
||||
var updatedStatusBarStyle = self.statusBarStyle
|
||||
if let top = self.state.top {
|
||||
self.applyLayout(layout: layout, to: top, transition: transition)
|
||||
self.statusBarStyle = top.value.statusBar.statusBarStyle
|
||||
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
|
||||
} else {
|
||||
self.statusBarStyle = .Ignore
|
||||
updatedStatusBarStyle = .Ignore
|
||||
}
|
||||
if self.statusBarStyle != updatedStatusBarStyle {
|
||||
self.statusBarStyle = updatedStatusBarStyle
|
||||
self.statusBarStyleUpdated?(statusBarTransition)
|
||||
}
|
||||
|
||||
if self.state.transition == nil {
|
||||
|
@ -368,6 +368,12 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
let flatContainer = NavigationContainer(controllerRemoved: { [weak self] controller in
|
||||
self?.controllerRemoved(controller)
|
||||
})
|
||||
flatContainer.statusBarStyleUpdated = { [weak self] transition in
|
||||
guard let strongSelf = self, let layout = strongSelf.validLayout else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateContainers(layout: layout, transition: transition)
|
||||
}
|
||||
self.displayNode.insertSubnode(flatContainer, at: 0)
|
||||
self.rootContainer = .flat(flatContainer)
|
||||
flatContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
@ -378,6 +384,12 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
let flatContainer = NavigationContainer(controllerRemoved: { [weak self] controller in
|
||||
self?.controllerRemoved(controller)
|
||||
})
|
||||
flatContainer.statusBarStyleUpdated = { [weak self] transition in
|
||||
guard let strongSelf = self, let layout = strongSelf.validLayout else {
|
||||
return
|
||||
}
|
||||
strongSelf.updateContainers(layout: layout, transition: transition)
|
||||
}
|
||||
self.displayNode.insertSubnode(flatContainer, at: 0)
|
||||
self.rootContainer = .flat(flatContainer)
|
||||
flatContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
@ -394,6 +406,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
self.rootContainer = .split(splitContainer)
|
||||
splitContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
splitContainer.update(layout: layout, masterControllers: masterControllers, detailControllers: detailControllers, transition: .immediate)
|
||||
flatContainer.statusBarStyleUpdated = nil
|
||||
flatContainer.removeFromSupernode()
|
||||
case let .split(splitContainer):
|
||||
transition.updateFrame(node: splitContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||
|
@ -34,7 +34,7 @@ private func importedAccountData(basePath: String, documentsPath: String, accoun
|
||||
}
|
||||
}
|
||||
|> ignoreValues
|
||||
|> introduceError(AccountImportError.self)
|
||||
|> castError(AccountImportError.self)
|
||||
|
||||
let importData = importPreferencesData(documentsPath: documentsPath, masterDatacenterId: Int32(masterDatacenterId), account: account, database: database)
|
||||
|> mapToSignal { accountUserId -> Signal<(AccountImportProgressType, Float), AccountImportError> in
|
||||
@ -65,7 +65,7 @@ private func importPreferencesData(documentsPath: String, masterDatacenterId: In
|
||||
transaction.setState(AuthorizedAccountState(isTestingEnvironment: false, masterDatacenterId: masterDatacenterId, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: parsedAccountUserId), state: nil))
|
||||
return parsedAccountUserId
|
||||
}
|
||||
|> introduceError(AccountImportError.self)
|
||||
|> castError(AccountImportError.self)
|
||||
} else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
@ -81,17 +81,17 @@ private func importDatabaseData(accountManager: AccountManager, account: Tempora
|
||||
transaction.updatePeerPresencesInternal(presences: [user.id: presence], merge: { _, updated in return updated })
|
||||
}
|
||||
|> ignoreValues
|
||||
|> introduceError(AccountImportError.self)
|
||||
|> castError(AccountImportError.self)
|
||||
}
|
||||
|
||||
let importedSecretChats = loadLegacySecretChats(account: account, basePath: basePath, accountPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: accountUserId), database: database)
|
||||
|> introduceError(AccountImportError.self)
|
||||
|> castError(AccountImportError.self)
|
||||
|
||||
/*let importedFiles = loadLegacyFiles(account: account, basePath: basePath, accountPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: accountUserId), database: database)
|
||||
|> introduceError(AccountImportError.self)*/
|
||||
|> castError(AccountImportError.self)*/
|
||||
|
||||
let importedLegacyPreferences = importLegacyPreferences(accountManager: accountManager, account: account, documentsPath: basePath + "/Documents", database: database)
|
||||
|> introduceError(AccountImportError.self)
|
||||
|> castError(AccountImportError.self)
|
||||
|
||||
return importedAccountUser
|
||||
|> map { _ -> (AccountImportProgressType, Float) in return (.generic, 0.0) }
|
||||
@ -224,7 +224,7 @@ public func importedLegacyAccount(basePath: String, accountManager: AccountManag
|
||||
}
|
||||
|
||||
return temporaryAccount(manager: accountManager, rootPath: rootPathForBasePath(basePath), encryptionParameters: encryptionParameters)
|
||||
|> introduceError(AccountImportError.self)
|
||||
|> castError(AccountImportError.self)
|
||||
|> mapToSignal { account -> Signal<ImportedLegacyAccountEvent, AccountImportError> in
|
||||
let actions = importedAccountData(basePath: basePath, documentsPath: documentsPath, accountManager: accountManager, account: account, database: database)
|
||||
var result = actions
|
||||
|
@ -209,7 +209,7 @@ public final class SecureIdAuthController: ViewController {
|
||||
|
||||
switch self.mode {
|
||||
case let .form(peerId, scope, publicKey, callbackUrl, _, _):
|
||||
self.formDisposable = (combineLatest(requestSecureIdForm(postbox: context.account.postbox, network: context.account.network, peerId: peerId, scope: scope, publicKey: publicKey), secureIdConfiguration(postbox: context.account.postbox, network: context.account.network) |> introduceError(RequestSecureIdFormError.self))
|
||||
self.formDisposable = (combineLatest(requestSecureIdForm(postbox: context.account.postbox, network: context.account.network, peerId: peerId, scope: scope, publicKey: publicKey), secureIdConfiguration(postbox: context.account.postbox, network: context.account.network) |> castError(RequestSecureIdFormError.self))
|
||||
|> mapToSignal { form, configuration -> Signal<SecureIdEncryptedFormData, RequestSecureIdFormError> in
|
||||
return context.account.postbox.transaction { transaction -> Signal<SecureIdEncryptedFormData, RequestSecureIdFormError> in
|
||||
guard let accountPeer = transaction.getPeer(context.account.peerId), let servicePeer = transaction.getPeer(form.peerId) else {
|
||||
@ -240,7 +240,7 @@ public final class SecureIdAuthController: ViewController {
|
||||
handleError(error, callbackUrl, peerId)
|
||||
})
|
||||
case .list:
|
||||
self.formDisposable = (combineLatest(getAllSecureIdValues(network: self.context.account.network), secureIdConfiguration(postbox: context.account.postbox, network: context.account.network) |> introduceError(GetAllSecureIdValuesError.self), context.account.postbox.transaction { transaction -> Signal<Peer, GetAllSecureIdValuesError> in
|
||||
self.formDisposable = (combineLatest(getAllSecureIdValues(network: self.context.account.network), secureIdConfiguration(postbox: context.account.postbox, network: context.account.network) |> castError(GetAllSecureIdValuesError.self), context.account.postbox.transaction { transaction -> Signal<Peer, GetAllSecureIdValuesError> in
|
||||
guard let accountPeer = transaction.getPeer(context.account.peerId) else {
|
||||
return .fail(.generic)
|
||||
}
|
||||
|
@ -337,7 +337,7 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI
|
||||
|
||||
return updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: resultPeerId)
|
||||
}
|
||||
|> introduceError(ChannelDiscussionGroupError.self)
|
||||
|> castError(ChannelDiscussionGroupError.self)
|
||||
|> switchToLatest
|
||||
}
|
||||
} else {
|
||||
|
@ -355,7 +355,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
|
||||
|
||||
addMembersDisposable.set((contactsController.result
|
||||
|> deliverOnMainQueue
|
||||
|> introduceError(AddChannelMemberError.self)
|
||||
|> castError(AddChannelMemberError.self)
|
||||
|> mapToSignal { [weak contactsController] contacts -> Signal<Never, AddChannelMemberError> in
|
||||
contactsController?.displayProgress = true
|
||||
|
||||
|
@ -1105,7 +1105,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
|
||||
}
|
||||
|
||||
let _ = (contactDataManager.createContactWithData(composedContactData)
|
||||
|> introduceError(AddContactError.self)
|
||||
|> castError(AddContactError.self)
|
||||
|> mapToSignal { contactIdAndData -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, AddContactError> in
|
||||
guard let (id, data) = contactIdAndData else {
|
||||
return .single(nil)
|
||||
@ -1122,7 +1122,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
|
||||
context.account.postbox.transaction { transaction -> (DeviceContactStableId, DeviceContactExtendedData, Peer?)? in
|
||||
return (id, data, transaction.getPeer(peer.id))
|
||||
}
|
||||
|> introduceError(AddContactError.self)
|
||||
|> castError(AddContactError.self)
|
||||
)
|
||||
}
|
||||
default:
|
||||
@ -1130,13 +1130,13 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
|
||||
}
|
||||
|
||||
return importContact(account: context.account, firstName: composedContactData.basicData.firstName, lastName: composedContactData.basicData.lastName, phoneNumber: filteredPhoneNumbers[0].value)
|
||||
|> introduceError(AddContactError.self)
|
||||
|> castError(AddContactError.self)
|
||||
|> mapToSignal { peerId -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, AddContactError> in
|
||||
if let peerId = peerId {
|
||||
return context.account.postbox.transaction { transaction -> (DeviceContactStableId, DeviceContactExtendedData, Peer?)? in
|
||||
return (id, data, transaction.getPeer(peerId))
|
||||
}
|
||||
|> introduceError(AddContactError.self)
|
||||
|> castError(AddContactError.self)
|
||||
} else {
|
||||
return .single((id, data, nil))
|
||||
}
|
||||
|
@ -1760,7 +1760,7 @@ public func chatMessagePhotoInteractiveFetched(context: AccountContext, photoRef
|
||||
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||
if case .remote = type, let peerType = storeToDownloadsPeerType {
|
||||
return storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: photoReference.abstract, peerType: peerType)
|
||||
|> introduceError(FetchResourceError.self)
|
||||
|> castError(FetchResourceError.self)
|
||||
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||
return .complete()
|
||||
}
|
||||
|
@ -54,6 +54,8 @@
|
||||
BOOL _isOpenGLLoaded;
|
||||
}
|
||||
|
||||
@property (nonatomic) CGRect defaultFrame;
|
||||
|
||||
- (instancetype)initWithBackgroundColor:(UIColor *)backgroundColor primaryColor:(UIColor *)primaryColor buttonColor:(UIColor *)buttonColor accentColor:(UIColor *)accentColor regularDotColor:(UIColor *)regularDotColor highlightedDotColor:(UIColor *)highlightedDotColor suggestedLocalizationSignal:(SSignal *)suggestedLocalizationSignal;
|
||||
|
||||
@property (nonatomic, copy) void (^startMessaging)(void);
|
||||
|
@ -71,6 +71,23 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
|
||||
}
|
||||
@end
|
||||
|
||||
@interface RMIntroView : UIView
|
||||
|
||||
@property (nonatomic, copy) void (^onLayout)();
|
||||
|
||||
@end
|
||||
|
||||
@implementation RMIntroView
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
if (_onLayout) {
|
||||
_onLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface RMIntroViewController () <UIGestureRecognizerDelegate>
|
||||
{
|
||||
@ -92,6 +109,8 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
|
||||
|
||||
SVariable *_alternativeLocalization;
|
||||
NSDictionary<NSString *, NSString *> *_englishStrings;
|
||||
|
||||
bool _loadedView;
|
||||
}
|
||||
@end
|
||||
|
||||
@ -215,12 +234,6 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
}
|
||||
|
||||
- (void)loadGL
|
||||
{
|
||||
if (/*[[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground*/true && !_isOpenGLLoaded)
|
||||
@ -273,10 +286,28 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
|
||||
_isOpenGLLoaded = false;
|
||||
}
|
||||
|
||||
- (void)loadView {
|
||||
self.view = [[RMIntroView alloc] initWithFrame:self.defaultFrame];
|
||||
__weak RMIntroViewController *weakSelf = self;
|
||||
((RMIntroView *)self.view).onLayout = ^{
|
||||
__strong RMIntroViewController *strongSelf = weakSelf;
|
||||
if (strongSelf != nil) {
|
||||
[strongSelf updateLayout];
|
||||
}
|
||||
};
|
||||
|
||||
[self viewDidLoad];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
if (_loadedView) {
|
||||
return;
|
||||
}
|
||||
_loadedView = true;
|
||||
|
||||
self.view.backgroundColor = _backgroundColor;
|
||||
|
||||
[self loadGL];
|
||||
@ -411,7 +442,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
|
||||
return deviceScreen;
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews
|
||||
- (void)updateLayout
|
||||
{
|
||||
UIInterfaceOrientation isVertical = (self.view.bounds.size.height / self.view.bounds.size.width > 1.0f);
|
||||
|
||||
|
@ -485,7 +485,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
if !wallets.wallets.isEmpty {
|
||||
let _ = (testGiverWalletAddress(tonInstance: tonContext.instance)
|
||||
|> deliverOnMainQueue).start(next: { address in
|
||||
arguments.pushController(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: wallets.wallets[0], address: address))
|
||||
arguments.pushController(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: wallets.wallets[0].info, address: address))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -500,7 +500,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
|> deliverOnMainQueue).start(next: { wallets in
|
||||
if let tonContext = context.tonContext {
|
||||
if !wallets.wallets.isEmpty {
|
||||
let _ = (walletAddress(publicKey: wallets.wallets[0].publicKey, tonInstance: tonContext.instance)
|
||||
let _ = (walletAddress(publicKey: wallets.wallets[0].info.publicKey, tonInstance: tonContext.instance)
|
||||
|> deliverOnMainQueue).start(next: { address in
|
||||
let _ = (getGramsFromTestGiver(address: address, amount: 1500000000, tonInstance: tonContext.instance)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
|
@ -1609,16 +1609,33 @@ private func accountContextMenuItems(context: AccountContext, logout: @escaping
|
||||
}
|
||||
|
||||
func openWallet(context: AccountContext, push: @escaping (ViewController) -> Void) {
|
||||
let _ = (availableWallets(postbox: context.account.postbox)
|
||||
|> deliverOnMainQueue).start(next: { wallets in
|
||||
if let tonContext = context.tonContext {
|
||||
if wallets.wallets.isEmpty {
|
||||
guard let tonContext = context.tonContext else {
|
||||
return
|
||||
}
|
||||
let _ = (combineLatest(queue: .mainQueue(),
|
||||
availableWallets(postbox: context.account.postbox),
|
||||
tonContext.keychain.encryptionPublicKey()
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { wallets, currentPublicKey in
|
||||
if wallets.wallets.isEmpty {
|
||||
if let _ = currentPublicKey {
|
||||
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .intro))
|
||||
} else {
|
||||
let _ = (walletAddress(publicKey: wallets.wallets[0].publicKey, tonInstance: tonContext.instance)
|
||||
|> deliverOnMainQueue).start(next: { address in
|
||||
push(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: wallets.wallets[0], address: address))
|
||||
})
|
||||
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .secureStorageNotAvailable))
|
||||
}
|
||||
} else {
|
||||
let walletInfo = wallets.wallets[0].info
|
||||
if let currentPublicKey = currentPublicKey {
|
||||
if currentPublicKey == walletInfo.encryptedSecret.publicKey {
|
||||
let _ = (walletAddress(publicKey: walletInfo.publicKey, tonInstance: tonContext.instance)
|
||||
|> deliverOnMainQueue).start(next: { address in
|
||||
push(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: walletInfo, address: address))
|
||||
})
|
||||
} else {
|
||||
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .secureStorageReset(.changed)))
|
||||
}
|
||||
} else {
|
||||
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .secureStorageReset(.notAvailable)))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -272,7 +272,7 @@ public func preparedShareItems(account: Account, to peerId: PeerId, dataItems: [
|
||||
dataSignals = dataSignals
|
||||
|> then(
|
||||
wrappedSignal
|
||||
|> introduceError(Void.self)
|
||||
|> castError(Void.self)
|
||||
|> take(1)
|
||||
)
|
||||
}
|
||||
@ -339,11 +339,11 @@ public func sentShareItems(account: Account, to peerIds: [PeerId], items: [Prepa
|
||||
}
|
||||
|
||||
return enqueueMessagesToMultiplePeers(account: account, peerIds: peerIds, messages: messages)
|
||||
|> introduceError(Void.self)
|
||||
|> castError(Void.self)
|
||||
|> mapToSignal { messageIds -> Signal<Float, Void> in
|
||||
let key: PostboxViewKey = .messages(Set(messageIds))
|
||||
return account.postbox.combinedView(keys: [key])
|
||||
|> introduceError(Void.self)
|
||||
|> castError(Void.self)
|
||||
|> mapToSignal { view -> Signal<Float, Void> in
|
||||
if let messagesView = view.views[key] as? MessagesView {
|
||||
for (_, message) in messagesView.messages {
|
||||
|
@ -11,11 +11,32 @@ import MtProtoKit
|
||||
import TelegramApi
|
||||
#endif
|
||||
|
||||
public struct TonKeychain {
|
||||
public let encrypt: (Data) -> Signal<Data?, NoError>
|
||||
public let decrypt: (Data) -> Signal<Data?, NoError>
|
||||
public struct TonKeychainEncryptedData: Codable, Equatable {
|
||||
public let publicKey: Data
|
||||
public let data: Data
|
||||
|
||||
public init(encrypt: @escaping (Data) -> Signal<Data?, NoError>, decrypt: @escaping (Data) -> Signal<Data?, NoError>) {
|
||||
public init(publicKey: Data, data: Data) {
|
||||
self.publicKey = publicKey
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
|
||||
public enum TonKeychainEncryptDataError {
|
||||
case generic
|
||||
}
|
||||
|
||||
public enum TonKeychainDecryptDataError {
|
||||
case generic
|
||||
case publicKeyMismatch
|
||||
}
|
||||
|
||||
public struct TonKeychain {
|
||||
public let encryptionPublicKey: () -> Signal<Data?, NoError>
|
||||
public let encrypt: (Data) -> Signal<TonKeychainEncryptedData, TonKeychainEncryptDataError>
|
||||
public let decrypt: (TonKeychainEncryptedData) -> Signal<Data, TonKeychainDecryptDataError>
|
||||
|
||||
public init(encryptionPublicKey: @escaping () -> Signal<Data?, NoError>, encrypt: @escaping (Data) -> Signal<TonKeychainEncryptedData, TonKeychainEncryptDataError>, decrypt: @escaping (TonKeychainEncryptedData) -> Signal<Data, TonKeychainDecryptDataError>) {
|
||||
self.encryptionPublicKey = encryptionPublicKey
|
||||
self.encrypt = encrypt
|
||||
self.decrypt = decrypt
|
||||
}
|
||||
@ -93,7 +114,7 @@ public final class TonInstance {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func createWallet(keychain: TonKeychain, serverSalt: Data) -> Signal<(WalletInfo, [String]), NoError> {
|
||||
fileprivate func createWallet(keychain: TonKeychain, serverSalt: Data) -> Signal<(WalletInfo, [String]), CreateWalletError> {
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
self.impl.with { impl in
|
||||
@ -104,17 +125,14 @@ public final class TonInstance {
|
||||
return
|
||||
}
|
||||
let cancel = keychain.encrypt(key.secret).start(next: { encryptedSecretData in
|
||||
guard let encryptedSecretData = encryptedSecretData else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
let _ = self.exportKey(key: key, serverSalt: serverSalt).start(next: { wordList in
|
||||
subscriber.putNext((WalletInfo(publicKey: WalletPublicKey(rawValue: key.publicKey), encryptedSecret: EncryptedWalletSecret(rawValue: encryptedSecretData)), wordList))
|
||||
subscriber.putNext((WalletInfo(publicKey: WalletPublicKey(rawValue: key.publicKey), encryptedSecret: encryptedSecretData), wordList))
|
||||
subscriber.putCompletion()
|
||||
}, error: { _ in
|
||||
preconditionFailure()
|
||||
}, error: { error in
|
||||
subscriber.putError(.generic)
|
||||
})
|
||||
}, error: { _ in
|
||||
subscriber.putError(.generic)
|
||||
}, completed: {
|
||||
})
|
||||
}, error: { _ in
|
||||
@ -142,30 +160,14 @@ public final class TonInstance {
|
||||
return
|
||||
}
|
||||
let cancel = keychain.encrypt(key.secret).start(next: { encryptedSecretData in
|
||||
guard let encryptedSecretData = encryptedSecretData else {
|
||||
subscriber.putError(.generic)
|
||||
return
|
||||
}
|
||||
subscriber.putNext(WalletInfo(publicKey: WalletPublicKey(rawValue: key.publicKey), encryptedSecret: EncryptedWalletSecret(rawValue: encryptedSecretData)))
|
||||
subscriber.putNext(WalletInfo(publicKey: WalletPublicKey(rawValue: key.publicKey), encryptedSecret: encryptedSecretData))
|
||||
subscriber.putCompletion()
|
||||
}, error: { _ in
|
||||
subscriber.putError(.generic)
|
||||
}, completed: {
|
||||
})
|
||||
}, error: { error in
|
||||
if let error = error as? TONError {
|
||||
#warning("Use error kind APIs")
|
||||
let filePrefix = "File \""
|
||||
let fileSuffix = "\" can't be created for writing"
|
||||
let text = error.text
|
||||
if text.hasPrefix(filePrefix) && text.hasSuffix(fileSuffix) {
|
||||
let filePath = String(error.text[text.index(text.startIndex, offsetBy: filePrefix.count) ..< text.index(text.endIndex, offsetBy: -fileSuffix.count)])
|
||||
subscriber.putError(.fileExists(filePath))
|
||||
} else {
|
||||
subscriber.putError(.generic)
|
||||
}
|
||||
} else {
|
||||
subscriber.putError(.generic)
|
||||
}
|
||||
}, error: { _ in
|
||||
subscriber.putError(.generic)
|
||||
}, completed: {
|
||||
})
|
||||
disposable.set(ActionDisposable {
|
||||
@ -253,10 +255,10 @@ public final class TonInstance {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func getWalletState(address: String) -> Signal<WalletState, NoError> {
|
||||
fileprivate func getWalletState(address: String) -> Signal<(WalletState, Int64), NoError> {
|
||||
return self.getWalletStateRaw(address: address)
|
||||
|> map { state in
|
||||
return WalletState(balance: state.balance, lastTransactionId: state.lastTransactionId.flatMap(WalletTransactionId.init(tonTransactionId:)))
|
||||
return (WalletState(balance: state.balance, lastTransactionId: state.lastTransactionId.flatMap(WalletTransactionId.init(tonTransactionId:))), state.syncUtime)
|
||||
}
|
||||
}
|
||||
|
||||
@ -424,12 +426,11 @@ public final class TonInstance {
|
||||
}
|
||||
|
||||
fileprivate func sendGramsFromWallet(keychain: TonKeychain, serverSalt: Data, walletInfo: WalletInfo, fromAddress: String, toAddress: String, amount: Int64, textMessage: String) -> Signal<Never, SendGramsFromWalletError> {
|
||||
return keychain.decrypt(walletInfo.encryptedSecret.rawValue)
|
||||
|> castError(SendGramsFromWalletError.self)
|
||||
return keychain.decrypt(walletInfo.encryptedSecret)
|
||||
|> mapError { _ -> SendGramsFromWalletError in
|
||||
return .secretDecryptionFailed
|
||||
}
|
||||
|> mapToSignal { decryptedSecret -> Signal<Never, SendGramsFromWalletError> in
|
||||
guard let decryptedSecret = decryptedSecret else {
|
||||
return .fail(.secretDecryptionFailed)
|
||||
}
|
||||
let key = TONKey(publicKey: walletInfo.publicKey.rawValue, secret: decryptedSecret)
|
||||
|
||||
return self.ensureWalletInitialized(address: fromAddress, key: key, serverSalt: serverSalt)
|
||||
@ -462,12 +463,11 @@ public final class TonInstance {
|
||||
}
|
||||
|
||||
fileprivate func walletRestoreWords(walletInfo: WalletInfo, keychain: TonKeychain, serverSalt: Data) -> Signal<[String], WalletRestoreWordsError> {
|
||||
return keychain.decrypt(walletInfo.encryptedSecret.rawValue)
|
||||
|> castError(WalletRestoreWordsError.self)
|
||||
return keychain.decrypt(walletInfo.encryptedSecret)
|
||||
|> mapError { _ -> WalletRestoreWordsError in
|
||||
return .secretDecryptionFailed
|
||||
}
|
||||
|> mapToSignal { decryptedSecret -> Signal<[String], WalletRestoreWordsError> in
|
||||
guard let decryptedSecret = decryptedSecret else {
|
||||
return .fail(.secretDecryptionFailed)
|
||||
}
|
||||
return Signal { subscriber in
|
||||
let disposable = MetaDisposable()
|
||||
|
||||
@ -504,31 +504,28 @@ public struct WalletPublicKey: Codable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct EncryptedWalletSecret: Codable, Equatable {
|
||||
public var rawValue: Data
|
||||
|
||||
public init(rawValue: Data) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
}
|
||||
|
||||
public struct WalletInfo: PostboxCoding, Codable, Equatable {
|
||||
public let publicKey: WalletPublicKey
|
||||
public let encryptedSecret: EncryptedWalletSecret
|
||||
public let encryptedSecret: TonKeychainEncryptedData
|
||||
|
||||
public init(publicKey: WalletPublicKey, encryptedSecret: EncryptedWalletSecret) {
|
||||
public init(publicKey: WalletPublicKey, encryptedSecret: TonKeychainEncryptedData) {
|
||||
self.publicKey = publicKey
|
||||
self.encryptedSecret = encryptedSecret
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.publicKey = WalletPublicKey(rawValue: decoder.decodeStringForKey("publicKey", orElse: ""))
|
||||
self.encryptedSecret = EncryptedWalletSecret(rawValue: decoder.decodeDataForKey("encryptedSecret")!)
|
||||
if let publicKey = decoder.decodeDataForKey("encryptedSecretPublicKey"), let secret = decoder.decodeDataForKey("encryptedSecretData") {
|
||||
self.encryptedSecret = TonKeychainEncryptedData(publicKey: publicKey, data: secret)
|
||||
} else {
|
||||
self.encryptedSecret = TonKeychainEncryptedData(publicKey: Data(), data: Data())
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeString(self.publicKey.rawValue, forKey: "publicKey")
|
||||
encoder.encodeData(self.encryptedSecret.rawValue, forKey: "encryptedSecret")
|
||||
encoder.encodeData(self.encryptedSecret.publicKey, forKey: "encryptedSecretPublicKey")
|
||||
encoder.encodeData(self.encryptedSecret.data, forKey: "encryptedSecretData")
|
||||
}
|
||||
}
|
||||
|
||||
@ -550,7 +547,7 @@ public struct WalletStateRecord: PostboxCoding, Equatable {
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.info = decoder.decodeDataForKey("info").flatMap { data in
|
||||
return try? JSONDecoder().decode(WalletInfo.self, from: data)
|
||||
} ?? WalletInfo(publicKey: WalletPublicKey(rawValue: ""), encryptedSecret: EncryptedWalletSecret(rawValue: Data()))
|
||||
} ?? WalletInfo(publicKey: WalletPublicKey(rawValue: ""), encryptedSecret: TonKeychainEncryptedData(publicKey: Data(), data: Data()))
|
||||
self.state = decoder.decodeDataForKey("state").flatMap { data in
|
||||
return try? JSONDecoder().decode(CombinedWalletState.self, from: data)
|
||||
}
|
||||
@ -617,7 +614,6 @@ public func createWallet(postbox: Postbox, network: Network, tonInstance: TonIns
|
||||
}
|
||||
|> mapToSignal { serverSalt -> Signal<(WalletInfo, [String]), CreateWalletError> in
|
||||
return tonInstance.createWallet(keychain: keychain, serverSalt: serverSalt)
|
||||
|> castError(CreateWalletError.self)
|
||||
|> mapToSignal { walletInfo, wordList -> Signal<(WalletInfo, [String]), CreateWalletError> in
|
||||
return postbox.transaction { transaction -> (WalletInfo, [String]) in
|
||||
transaction.updatePreferencesEntry(key: PreferencesKeys.walletCollection, { current in
|
||||
@ -634,7 +630,6 @@ public func createWallet(postbox: Postbox, network: Network, tonInstance: TonIns
|
||||
|
||||
private enum ImportWalletInternalError {
|
||||
case generic
|
||||
case fileExists(String)
|
||||
}
|
||||
|
||||
public enum ImportWalletError {
|
||||
@ -652,12 +647,6 @@ public func importWallet(postbox: Postbox, network: Network, tonInstance: TonIns
|
||||
switch error {
|
||||
case .generic:
|
||||
return .fail(.generic)
|
||||
case let .fileExists(path):
|
||||
let _ = try? FileManager.default.removeItem(atPath: path)
|
||||
return tonInstance.importWallet(keychain: keychain, wordList: wordList, serverSalt: serverSalt)
|
||||
|> mapError { _ -> ImportWalletError in
|
||||
return .generic
|
||||
}
|
||||
}
|
||||
}
|
||||
|> mapToSignal { walletInfo -> Signal<WalletInfo, ImportWalletError> in
|
||||
@ -718,7 +707,7 @@ public func testGiverWalletAddress(tonInstance: TonInstance) -> Signal<String, N
|
||||
return tonInstance.testGiverWalletAddress()
|
||||
}
|
||||
|
||||
public func getWalletState(address: String, tonInstance: TonInstance) -> Signal<WalletState, NoError> {
|
||||
private func getWalletState(address: String, tonInstance: TonInstance) -> Signal<(WalletState, Int64), NoError> {
|
||||
return tonInstance.getWalletState(address: address)
|
||||
}
|
||||
|
||||
@ -750,7 +739,7 @@ public func getCombinedWalletState(postbox: Postbox, walletInfo: WalletInfo, ton
|
||||
|> mapToSignal { address -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
||||
return getWalletState(address: address, tonInstance: tonInstance)
|
||||
|> castError(GetCombinedWalletStateError.self)
|
||||
|> mapToSignal { walletState -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
||||
|> mapToSignal { walletState, syncUtime -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
||||
let topTransactions: Signal<[WalletTransaction], GetCombinedWalletStateError>
|
||||
if walletState.lastTransactionId == cachedState?.walletState.lastTransactionId {
|
||||
topTransactions = .single(cachedState?.topTransactions ?? [])
|
||||
@ -762,7 +751,7 @@ public func getCombinedWalletState(postbox: Postbox, walletInfo: WalletInfo, ton
|
||||
}
|
||||
return topTransactions
|
||||
|> mapToSignal { topTransactions -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
||||
let combinedState = CombinedWalletState(walletState: walletState, timestamp: 0, topTransactions: topTransactions)
|
||||
let combinedState = CombinedWalletState(walletState: walletState, timestamp: syncUtime, topTransactions: topTransactions)
|
||||
return postbox.transaction { transaction -> CombinedWalletStateResult in
|
||||
transaction.updatePreferencesEntry(key: PreferencesKeys.walletCollection, { current in
|
||||
var walletCollection = (current as? WalletCollection) ?? WalletCollection(wallets: [])
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -650,7 +650,9 @@ final class SharedApplicationContext {
|
||||
let tonKeychain: TonKeychain
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
tonKeychain = TonKeychain(encrypt: { data in
|
||||
tonKeychain = TonKeychain(encryptionPublicKey: {
|
||||
return .single(Data())
|
||||
}, encrypt: { data in
|
||||
return Signal { subscriber in
|
||||
subscriber.putNext(data)
|
||||
subscriber.putCompletion()
|
||||
@ -664,18 +666,34 @@ final class SharedApplicationContext {
|
||||
}
|
||||
})
|
||||
#else
|
||||
tonKeychain = TonKeychain(encrypt: { data in
|
||||
tonKeychain = TonKeychain(encryptionPublicKey: {
|
||||
return Signal { subscriber in
|
||||
BuildConfig.encryptApplicationSecret(data, baseAppBundleId: baseAppBundleId, completion: { result in
|
||||
subscriber.putNext(result)
|
||||
BuildConfig.getHardwareEncryptionAvailable(withBaseAppBundleId: baseAppBundleId, completion: { value in
|
||||
subscriber.putNext(value)
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
return EmptyDisposable
|
||||
}
|
||||
}, decrypt: { data in
|
||||
}, encrypt: { data in
|
||||
return Signal { subscriber in
|
||||
BuildConfig.decryptApplicationSecret(data, baseAppBundleId: baseAppBundleId, completion: { result in
|
||||
subscriber.putNext(result)
|
||||
BuildConfig.encryptApplicationSecret(data, baseAppBundleId: baseAppBundleId, completion: { result, publicKey in
|
||||
if let result = result, let publicKey = publicKey {
|
||||
subscriber.putNext(TonKeychainEncryptedData(publicKey: publicKey, data: result))
|
||||
subscriber.putCompletion()
|
||||
} else {
|
||||
subscriber.putError(.generic)
|
||||
}
|
||||
})
|
||||
return EmptyDisposable
|
||||
}
|
||||
}, decrypt: { encryptedData in
|
||||
return Signal { subscriber in
|
||||
BuildConfig.decryptApplicationSecret(encryptedData.data, publicKey: encryptedData.publicKey, baseAppBundleId: baseAppBundleId, completion: { result in
|
||||
if let result = result {
|
||||
subscriber.putNext(result)
|
||||
} else {
|
||||
subscriber.putError(.generic)
|
||||
}
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
return EmptyDisposable
|
||||
@ -1319,14 +1337,6 @@ final class SharedApplicationContext {
|
||||
self.isInForegroundPromise.set(true)
|
||||
self.isActiveValue = true
|
||||
self.isActivePromise.set(true)
|
||||
|
||||
let configuration = URLSessionConfiguration.background(withIdentifier: "org.telegram.Telegram-iOS.background")
|
||||
let session = URLSession(configuration: configuration)
|
||||
if #available(iOS 9.0, *) {
|
||||
session.getAllTasks(completionHandler: { tasks in
|
||||
print(tasks)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
|
@ -22,6 +22,8 @@ final class AuthorizationSequenceSplashController: ViewController {
|
||||
|
||||
private let controller: RMIntroViewController
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
var nextPressed: ((PresentationStrings?) -> Void)?
|
||||
|
||||
private let suggestedLocalization = Promise<SuggestedLocalizationInfo?>()
|
||||
@ -102,7 +104,9 @@ final class AuthorizationSequenceSplashController: ViewController {
|
||||
private func addControllerIfNeeded() {
|
||||
if !controller.isViewLoaded || controller.view.superview == nil {
|
||||
self.displayNode.view.addSubview(controller.view)
|
||||
controller.view.frame = self.displayNode.bounds;
|
||||
if let layout = self.validLayout {
|
||||
controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
}
|
||||
controller.viewDidAppear(false)
|
||||
}
|
||||
}
|
||||
@ -134,14 +138,18 @@ final class AuthorizationSequenceSplashController: ViewController {
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.validLayout = layout
|
||||
let controllerFrame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
self.controller.defaultFrame = controllerFrame
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition)
|
||||
|
||||
self.addControllerIfNeeded()
|
||||
if case .immediate = transition {
|
||||
self.controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
self.controller.view.frame = controllerFrame
|
||||
} else {
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
self.controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
self.controller.view.frame = controllerFrame
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import AccountContext
|
||||
|
||||
func preloadedChatHistoryViewForLocation(_ location: ChatHistoryLocationInput, account: Account, chatLocation: ChatLocation, fixedCombinedReadStates: MessageHistoryViewReadState?, tagMask: MessageTags?, additionalData: [AdditionalMessageHistoryViewData], orderStatistics: MessageHistoryViewOrderStatistics = []) -> Signal<ChatHistoryViewUpdate, NoError> {
|
||||
return chatHistoryViewForLocation(location, account: account, chatLocation: chatLocation, scheduled: false, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, additionalData: additionalData, orderStatistics: orderStatistics)
|
||||
|> introduceError(Bool.self)
|
||||
|> castError(Bool.self)
|
||||
|> mapToSignal { update -> Signal<ChatHistoryViewUpdate, Bool> in
|
||||
switch update {
|
||||
case let .Loading(value):
|
||||
|
@ -79,7 +79,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
||||
let stickerSettings: StickerSettings = (transaction.getSharedData(ApplicationSpecificSharedDataKeys.stickerSettings) as? StickerSettings) ?? .defaultSettings
|
||||
return stickerSettings
|
||||
}
|
||||
|> introduceError(ChatContextQueryError.self)
|
||||
|> castError(ChatContextQueryError.self)
|
||||
|> mapToSignal { stickerSettings -> Signal<[FoundStickerItem], ChatContextQueryError> in
|
||||
let scope: SearchStickersScope
|
||||
switch stickerSettings.emojiStickerSuggestionMode {
|
||||
@ -91,7 +91,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
||||
scope = [.installed]
|
||||
}
|
||||
return searchStickers(account: context.account, query: query.basicEmoji.0, scope: scope)
|
||||
|> introduceError(ChatContextQueryError.self)
|
||||
|> castError(ChatContextQueryError.self)
|
||||
}
|
||||
|> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
return { _ in
|
||||
@ -123,7 +123,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
||||
}
|
||||
return { _ in return .hashtags(result) }
|
||||
}
|
||||
|> introduceError(ChatContextQueryError.self)
|
||||
|> castError(ChatContextQueryError.self)
|
||||
|
||||
return signal |> then(hashtags)
|
||||
case let .mention(query, types):
|
||||
@ -178,7 +178,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
||||
}
|
||||
return { _ in return .mentions(sortedPeers) }
|
||||
}
|
||||
|> introduceError(ChatContextQueryError.self)
|
||||
|> castError(ChatContextQueryError.self)
|
||||
|
||||
return signal |> then(participants)
|
||||
case let .command(query):
|
||||
@ -207,7 +207,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
||||
let sortedCommands = filteredCommands
|
||||
return { _ in return .commands(sortedCommands) }
|
||||
}
|
||||
|> introduceError(ChatContextQueryError.self)
|
||||
|> castError(ChatContextQueryError.self)
|
||||
return signal |> then(commands)
|
||||
case let .contextRequest(addressName, query):
|
||||
var delayRequest = true
|
||||
@ -239,7 +239,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
||||
return .single(nil)
|
||||
}
|
||||
}
|
||||
|> introduceError(ChatContextQueryError.self)
|
||||
|> castError(ChatContextQueryError.self)
|
||||
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> in
|
||||
if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
|
||||
let contextResults = requestChatContextResults(account: context.account, botId: user.id, peerId: chatPeer.id, query: query, offset: "")
|
||||
@ -307,7 +307,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
||||
|> map { result -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
return { _ in return .emojis(result, range) }
|
||||
}
|
||||
|> introduceError(ChatContextQueryError.self)
|
||||
|> castError(ChatContextQueryError.self)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -378,7 +378,7 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
|
||||
}
|
||||
|
||||
createSignal = addressPromise.get()
|
||||
|> introduceError(CreateGroupError.self)
|
||||
|> castError(CreateGroupError.self)
|
||||
|> mapToSignal { address -> Signal<PeerId?, CreateGroupError> in
|
||||
guard let address = address else {
|
||||
return .complete()
|
||||
|
@ -291,7 +291,7 @@ func fetchEmojiSpriteResource(postbox: Postbox, network: Network, resource: Emoj
|
||||
let packName = "P\(resource.packId)_by_AEStickerBot"
|
||||
|
||||
return loadedStickerPack(postbox: postbox, network: network, reference: .name(packName), forceActualized: false)
|
||||
|> introduceError(MediaResourceDataFetchError.self)
|
||||
|> castError(MediaResourceDataFetchError.self)
|
||||
|> mapToSignal { result -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
|
||||
switch result {
|
||||
case let .result(_, items, _):
|
||||
|
@ -218,7 +218,7 @@ private final class FetchManagerCategoryContext {
|
||||
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
|
||||
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|
||||
|> introduceError(FetchResourceError.self)
|
||||
|> castError(FetchResourceError.self)
|
||||
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||
return .complete()
|
||||
}
|
||||
@ -347,7 +347,7 @@ private final class FetchManagerCategoryContext {
|
||||
activeContext.disposable?.dispose()
|
||||
if isVideoPreload {
|
||||
activeContext.disposable = (preloadVideoResource(postbox: self.postbox, resourceReference: entry.resourceReference, duration: 4.0)
|
||||
|> introduceError(FetchResourceError.self)
|
||||
|> castError(FetchResourceError.self)
|
||||
|> map { _ -> FetchResourceSourceType in return .local }
|
||||
|> then(.single(.local))
|
||||
|> deliverOnMainQueue).start(next: { _ in
|
||||
@ -359,7 +359,7 @@ private final class FetchManagerCategoryContext {
|
||||
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
|
||||
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|
||||
|> introduceError(FetchResourceError.self)
|
||||
|> castError(FetchResourceError.self)
|
||||
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||
return .complete()
|
||||
}
|
||||
|
@ -224,7 +224,7 @@ public func fetchVideoLibraryMediaResource(postbox: Postbox, resource: VideoLibr
|
||||
|> map { view in
|
||||
return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue
|
||||
}
|
||||
|> introduceError(MediaResourceDataFetchError.self)
|
||||
|> castError(MediaResourceDataFetchError.self)
|
||||
|> mapToSignal { appConfiguration -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
|
||||
let config = VideoConversionConfiguration.with(appConfiguration: appConfiguration)
|
||||
let signal = Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> { subscriber in
|
||||
@ -343,7 +343,7 @@ func fetchLocalFileVideoMediaResource(postbox: Postbox, resource: LocalFileVideo
|
||||
|> map { view in
|
||||
return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue
|
||||
}
|
||||
|> introduceError(MediaResourceDataFetchError.self)
|
||||
|> castError(MediaResourceDataFetchError.self)
|
||||
|> mapToSignal { appConfiguration -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
|
||||
let config = VideoConversionConfiguration.with(appConfiguration: appConfiguration)
|
||||
let signal = Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> { subscriber in
|
||||
|
Binary file not shown.
@ -179,7 +179,7 @@ public class ShareRootControllerImpl {
|
||||
let account: Signal<(SharedAccountContextImpl, Account, [AccountWithInfo]), ShareAuthorizationError> = internalContext.sharedContext.accountManager.transaction { transaction -> (SharedAccountContextImpl, LoggingSettings) in
|
||||
return (internalContext.sharedContext, transaction.getSharedData(SharedDataKeys.loggingSettings) as? LoggingSettings ?? LoggingSettings.defaultSettings)
|
||||
}
|
||||
|> introduceError(ShareAuthorizationError.self)
|
||||
|> castError(ShareAuthorizationError.self)
|
||||
|> mapToSignal { sharedContext, loggingSettings -> Signal<(SharedAccountContextImpl, Account, [AccountWithInfo]), ShareAuthorizationError> in
|
||||
Logger.shared.logToFile = loggingSettings.logToFile
|
||||
Logger.shared.logToConsole = loggingSettings.logToConsole
|
||||
@ -187,7 +187,7 @@ public class ShareRootControllerImpl {
|
||||
Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData
|
||||
|
||||
return sharedContext.activeAccountsWithInfo
|
||||
|> introduceError(ShareAuthorizationError.self)
|
||||
|> castError(ShareAuthorizationError.self)
|
||||
|> take(1)
|
||||
|> mapToSignal { primary, accounts -> Signal<(SharedAccountContextImpl, Account, [AccountWithInfo]), ShareAuthorizationError> in
|
||||
guard let primary = primary else {
|
||||
@ -209,7 +209,7 @@ public class ShareRootControllerImpl {
|
||||
return combineLatest(sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]), limitsConfiguration, sharedContext.accountManager.accessChallengeData())
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|> introduceError(ShareAuthorizationError.self)
|
||||
|> castError(ShareAuthorizationError.self)
|
||||
|> map { sharedData, limitsConfiguration, data -> (AccountContext, PostboxAccessChallengeData, [AccountWithInfo]) in
|
||||
updateLegacyLocalization(strings: sharedContext.currentPresentationData.with({ $0 }).strings)
|
||||
let context = AccountContextImpl(sharedContext: sharedContext, account: account, tonContext: nil, limitsConfiguration: limitsConfiguration)
|
||||
|
@ -24,7 +24,7 @@ func preloadVideoResource(postbox: Postbox, resourceReference: MediaResourceRefe
|
||||
if let videoBuffer = result?.buffers.videoBuffer, let impl = source.syncWith({ $0 }) {
|
||||
return impl.ensureHasFrames(until: min(duration, videoBuffer.duration.seconds))
|
||||
|> ignoreValues
|
||||
|> introduceError(MediaFrameSourceSeekError.self)
|
||||
|> castError(MediaFrameSourceSeekError.self)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
|
@ -119,7 +119,9 @@ final class WalletInfoEmptyItemNode: ListViewItemNode {
|
||||
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let (addressLayout, addressApply) = makeAddressLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.address, font: Font.monospace(16.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets()))
|
||||
var addressString = item.address
|
||||
addressString.insert("\n", at: addressString.index(addressString.startIndex, offsetBy: addressString.count / 2))
|
||||
let (addressLayout, addressApply) = makeAddressLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: addressString, font: Font.monospace(16.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentVerticalOrigin: CGFloat = 32.0
|
||||
|
||||
|
@ -11,6 +11,59 @@ import SolidRoundedButtonNode
|
||||
import AnimationUI
|
||||
import SwiftSignalKit
|
||||
import MergeLists
|
||||
import TelegramStringFormatting
|
||||
|
||||
private func stringForRelativeUpdateTime(strings: PresentationStrings, day: RelativeTimestampFormatDay, dateTimeFormat: PresentationDateTimeFormat, hours: Int32, minutes: Int32) -> String {
|
||||
let dayString: String
|
||||
switch day {
|
||||
case .today:
|
||||
dayString = strings.Updated_TodayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0
|
||||
case .yesterday:
|
||||
dayString = strings.Updated_YesterdayAt(stringForShortTimestamp(hours: hours, minutes: minutes, dateTimeFormat: dateTimeFormat)).0
|
||||
}
|
||||
return dayString
|
||||
}
|
||||
|
||||
private func lastUpdateTimestampString(strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, statusTimestamp: Int32, relativeTo timestamp: Int32) -> String {
|
||||
let difference = timestamp - statusTimestamp
|
||||
let expanded = true
|
||||
if difference < 60 {
|
||||
return strings.Updated_JustNow
|
||||
} else if difference < 60 * 60 && !expanded {
|
||||
let minutes = difference / 60
|
||||
return strings.Updated_MinutesAgo(minutes)
|
||||
} else {
|
||||
var t: time_t = time_t(statusTimestamp)
|
||||
var timeinfo: tm = tm()
|
||||
localtime_r(&t, &timeinfo)
|
||||
|
||||
var now: time_t = time_t(timestamp)
|
||||
var timeinfoNow: tm = tm()
|
||||
localtime_r(&now, &timeinfoNow)
|
||||
|
||||
if timeinfo.tm_year != timeinfoNow.tm_year {
|
||||
return strings.Updated_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).0
|
||||
}
|
||||
|
||||
let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday
|
||||
if dayDifference == 0 || dayDifference == -1 {
|
||||
let day: RelativeTimestampFormatDay
|
||||
if dayDifference == 0 {
|
||||
if expanded {
|
||||
day = .today
|
||||
} else {
|
||||
let minutes = difference / (60 * 60)
|
||||
return strings.Updated_HoursAgo(minutes)
|
||||
}
|
||||
} else {
|
||||
day = .yesterday
|
||||
}
|
||||
return stringForRelativeUpdateTime(strings: strings, day: day, dateTimeFormat: dateTimeFormat, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min)
|
||||
} else {
|
||||
return strings.Updated_AtDate(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year, dateTimeFormat: dateTimeFormat)).0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class WalletInfoScreen: ViewController {
|
||||
private let context: AccountContext
|
||||
@ -20,6 +73,11 @@ public final class WalletInfoScreen: ViewController {
|
||||
|
||||
private var presentationData: PresentationData
|
||||
|
||||
private let _ready = Promise<Bool>()
|
||||
override public var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
public init(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, address: String) {
|
||||
self.context = context
|
||||
self.tonContext = tonContext
|
||||
@ -77,6 +135,8 @@ public final class WalletInfoScreen: ViewController {
|
||||
})
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self._ready.set((self.displayNode as! WalletInfoScreenNode).contentReady.get())
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
@ -126,11 +186,7 @@ private final class WalletInfoBalanceNode: ASDisplayNode {
|
||||
|
||||
let balanceTextFrame = CGRect(origin: balanceOrigin, size: balanceTextSize)
|
||||
let balanceIconFrame: CGRect
|
||||
if self.isLoading {
|
||||
balanceIconFrame = CGRect(origin: CGPoint(x: floor((width - balanceIconSize.width) / 2.0), y: balanceTextFrame.minY + floor((balanceTextFrame.height - balanceIconSize.height) / 2.0)), size: balanceIconSize)
|
||||
} else {
|
||||
balanceIconFrame = CGRect(origin: CGPoint(x: balanceTextFrame.maxX + balanceIconSpacing, y: balanceTextFrame.minY + floor((balanceTextFrame.height - balanceIconSize.height) / 2.0)), size: balanceIconSize)
|
||||
}
|
||||
balanceIconFrame = CGRect(origin: CGPoint(x: balanceTextFrame.maxX + balanceIconSpacing, y: balanceTextFrame.minY + floor((balanceTextFrame.height - balanceIconSize.height) / 2.0)), size: balanceIconSize)
|
||||
transition.updateFrameAdditive(node: self.balanceTextNode, frame: balanceTextFrame)
|
||||
transition.updateFrame(node: self.balanceIconNode, frame: balanceIconFrame)
|
||||
|
||||
@ -140,20 +196,33 @@ private final class WalletInfoBalanceNode: ASDisplayNode {
|
||||
|
||||
private final class WalletInfoHeaderNode: ASDisplayNode {
|
||||
var balance: Int64?
|
||||
var isRefreshing: Bool = false
|
||||
|
||||
var timestampString: String = "" {
|
||||
didSet {
|
||||
self.balanceTimestampNode.attributedText = NSAttributedString(string: self.timestampString, font: Font.regular(13), textColor: UIColor(white: 1.0, alpha: 0.6))
|
||||
}
|
||||
}
|
||||
|
||||
let balanceNode: WalletInfoBalanceNode
|
||||
private let refreshNode: AnimatedStickerNode
|
||||
private let balanceSubtitleNode: ImmediateTextNode
|
||||
private let balanceTimestampNode: ImmediateTextNode
|
||||
private let receiveButtonNode: SolidRoundedButtonNode
|
||||
private let sendButtonNode: SolidRoundedButtonNode
|
||||
private let headerBackgroundNode: ASImageNode
|
||||
|
||||
init(theme: PresentationTheme, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void) {
|
||||
init(account: Account, theme: PresentationTheme, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void) {
|
||||
self.balanceNode = WalletInfoBalanceNode(theme: theme)
|
||||
|
||||
self.balanceSubtitleNode = ImmediateTextNode()
|
||||
self.balanceSubtitleNode.displaysAsynchronously = false
|
||||
self.balanceSubtitleNode.attributedText = NSAttributedString(string: "your balance", font: Font.regular(13), textColor: UIColor(white: 1.0, alpha: 0.6))
|
||||
|
||||
self.balanceTimestampNode = ImmediateTextNode()
|
||||
self.balanceTimestampNode.displaysAsynchronously = false
|
||||
self.balanceTimestampNode.attributedText = NSAttributedString(string: "", font: Font.regular(13), textColor: UIColor(white: 1.0, alpha: 0.6))
|
||||
|
||||
self.headerBackgroundNode = ASImageNode()
|
||||
self.headerBackgroundNode.displaysAsynchronously = false
|
||||
self.headerBackgroundNode.displayWithoutProcessing = true
|
||||
@ -168,6 +237,13 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
|
||||
self.receiveButtonNode = SolidRoundedButtonNode(title: "Receive", icon: UIImage(bundleImageName: "Wallet/ReceiveButtonIcon"), theme: SolidRoundedButtonTheme(backgroundColor: .white, foregroundColor: .black), height: 50.0, cornerRadius: 10.0, gloss: false)
|
||||
self.sendButtonNode = SolidRoundedButtonNode(title: "Send", icon: UIImage(bundleImageName: "Wallet/SendButtonIcon"), theme: SolidRoundedButtonTheme(backgroundColor: .white, foregroundColor: .black), height: 50.0, cornerRadius: 10.0, gloss: false)
|
||||
|
||||
self.refreshNode = AnimatedStickerNode()
|
||||
self.refreshNode.playToCompletionOnStop = true
|
||||
self.refreshNode.automaticallyLoadFirstFrame = true
|
||||
if let path = getAppBundle().path(forResource: "celebrate", ofType: "tgs") {
|
||||
self.refreshNode.setup(account: account, resource: .localFile(path), width: Int(32.0 * UIScreenScale), height: Int(32.0 * UIScreenScale), mode: .direct)
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.headerBackgroundNode)
|
||||
@ -175,6 +251,8 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
|
||||
self.addSubnode(self.sendButtonNode)
|
||||
self.addSubnode(self.balanceNode)
|
||||
self.addSubnode(self.balanceSubtitleNode)
|
||||
self.addSubnode(self.balanceTimestampNode)
|
||||
self.addSubnode(self.refreshNode)
|
||||
|
||||
self.receiveButtonNode.pressed = {
|
||||
receiveAction()
|
||||
@ -206,11 +284,13 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
|
||||
let buttonAlpha = buttonTransition * 1.0
|
||||
|
||||
let balanceSubtitleSize = self.balanceSubtitleNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: 200.0))
|
||||
let balanceTimestampSize = self.balanceTimestampNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: 200.0))
|
||||
|
||||
let balanceHeight = self.balanceNode.update(width: size.width, transition: transition)
|
||||
let balanceSize = CGSize(width: size.width, height: balanceHeight)
|
||||
|
||||
let minHeaderScale: CGFloat = 0.435
|
||||
let maxHeaderScale: CGFloat = min(1.0, (size.width - 40.0) / balanceSize.width)
|
||||
let minHeaderScale: CGFloat = min(0.435, (size.width - 80.0 * 2.0) / balanceSize.width)
|
||||
|
||||
let minHeaderHeight: CGFloat = balanceSize.height + balanceSubtitleSize.height + balanceSubtitleSpacing
|
||||
|
||||
@ -219,13 +299,39 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
|
||||
let headerScaleTransition: CGFloat = max(0.0, min(1.0, (effectiveOffset - minHeaderOffset) / (maxHeaderOffset - minHeaderOffset)))
|
||||
let headerPositionTransition: CGFloat = max(0.0, (effectiveOffset - minHeaderOffset) / (maxOffset - minHeaderOffset))
|
||||
let headerY = headerPositionTransition * maxHeaderY + (1.0 - headerPositionTransition) * minHeaderY
|
||||
let headerScale = headerScaleTransition * 1.0 + (1.0 - headerScaleTransition) * minHeaderScale
|
||||
let headerScale = headerScaleTransition * maxHeaderScale + (1.0 - headerScaleTransition) * minHeaderScale
|
||||
|
||||
let refreshSize = CGSize(width: 32.0, height: 32.0)
|
||||
self.refreshNode.updateLayout(size: refreshSize)
|
||||
transition.updateFrame(node: self.refreshNode, frame: CGRect(origin: CGPoint(x: floor((size.width - refreshSize.width) / 2.0), y: navigationHeight - 44.0 + floor((44.0 - refreshSize.height) / 2.0)), size: refreshSize))
|
||||
if self.balance == nil {
|
||||
transition.updateAlpha(node: self.refreshNode, alpha: 0.0)
|
||||
transition.updateSublayerTransformScale(node: self.refreshNode, scale: 0.1)
|
||||
self.refreshNode.visibility = false
|
||||
} else if self.isRefreshing {
|
||||
transition.updateAlpha(node: self.refreshNode, alpha: 1.0)
|
||||
transition.updateSublayerTransformScale(node: self.refreshNode, scale: 1.0)
|
||||
self.refreshNode.visibility = true
|
||||
} else {
|
||||
let refreshOffset: CGFloat = 20.0
|
||||
let refreshScaleTransition: CGFloat = max(0.0, min(1.0, (offset - maxOffset) / refreshOffset))
|
||||
transition.updateAlpha(node: self.refreshNode, alpha: refreshScaleTransition)
|
||||
let refreshScale: CGFloat = refreshScaleTransition * 1.0 + (1.0 - refreshScaleTransition) * 0.1
|
||||
transition.updateSublayerTransformScale(node: self.refreshNode, scale: refreshScale)
|
||||
self.refreshNode.visibility = false
|
||||
}
|
||||
|
||||
let balanceFrame = CGRect(origin: CGPoint(x: 0.0, y: headerY), size: balanceSize)
|
||||
transition.updateFrame(node: self.balanceNode, frame: balanceFrame)
|
||||
transition.updateSublayerTransformScale(node: self.balanceNode, scale: headerScale)
|
||||
|
||||
transition.updateFrameAdditive(node: self.balanceSubtitleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - balanceSubtitleSize.width) / 2.0), y: balanceFrame.midY + (balanceFrame.height / 2.0 * headerScale) + balanceSubtitleSpacing), size: balanceSubtitleSize))
|
||||
let balanceSubtitleFrame = CGRect(origin: CGPoint(x: floor((size.width - balanceSubtitleSize.width) / 2.0), y: balanceFrame.midY + (balanceFrame.height / 2.0 * headerScale) + balanceSubtitleSpacing), size: balanceSubtitleSize)
|
||||
transition.updateFrameAdditive(node: self.balanceSubtitleNode, frame: balanceSubtitleFrame)
|
||||
|
||||
let balanceTimestampFrame = CGRect(origin: CGPoint(x: floor((size.width - balanceTimestampSize.width) / 2.0), y: balanceSubtitleFrame.maxY + 2.0), size: balanceTimestampSize)
|
||||
transition.updateFrameAdditive(node: self.balanceTimestampNode, frame: balanceTimestampFrame)
|
||||
|
||||
transition.updateAlpha(node: self.balanceTimestampNode, alpha: headerScaleTransition)
|
||||
|
||||
let headerHeight: CGFloat = 1000.0
|
||||
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: effectiveOffset + 10.0 - headerHeight), size: CGSize(width: size.width, height: headerHeight)))
|
||||
@ -252,11 +358,13 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
if self.balance == nil {
|
||||
self.balanceNode.balanceTextNode.isHidden = true
|
||||
self.balanceNode.isHidden = true
|
||||
self.balanceSubtitleNode.isHidden = true
|
||||
self.balanceTimestampNode.isHidden = true
|
||||
} else {
|
||||
self.balanceNode.balanceTextNode.isHidden = false
|
||||
self.balanceNode.isHidden = false
|
||||
self.balanceSubtitleNode.isHidden = false
|
||||
self.balanceTimestampNode.isHidden = false
|
||||
}
|
||||
transition.updateFrame(node: self.receiveButtonNode, frame: receiveButtonFrame)
|
||||
transition.updateAlpha(node: self.receiveButtonNode, alpha: buttonAlpha)
|
||||
@ -276,12 +384,19 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.sendButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.receiveButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
func becameReady(animated: Bool) {
|
||||
if animated {
|
||||
self.sendButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.receiveButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.balanceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.balanceSubtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.balanceTimestampNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
self.balanceNode.isLoading = false
|
||||
self.balanceNode.balanceTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.balanceSubtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
func animateBeganRefreshing() {
|
||||
//self.refreshNode.layer.animate(from: 0.5 as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, delay: 0.0, removeOnCompletion: true, additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -358,24 +473,34 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
||||
private let address: String
|
||||
private let openTransaction: (WalletTransaction) -> Void
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
private let headerNode: WalletInfoHeaderNode
|
||||
private let listNode: ListView
|
||||
private let loadingIndicator: UIActivityIndicatorView
|
||||
|
||||
private var enqueuedTransactions: [WalletInfoListTransaction] = []
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private let balanceDisposable = MetaDisposable()
|
||||
private let stateDisposable = MetaDisposable()
|
||||
private let transactionListDisposable = MetaDisposable()
|
||||
|
||||
private var listOffset: CGFloat?
|
||||
private var reloadingState: Bool = false
|
||||
private var loadingMoreTransactions: Bool = false
|
||||
private var canLoadMoreTransactions: Bool = true
|
||||
|
||||
private var combinedState: CombinedWalletState?
|
||||
private var currentEntries: [WalletInfoListEntry]?
|
||||
|
||||
private var isReady: Bool = false
|
||||
|
||||
let contentReady = Promise<Bool>()
|
||||
private var didSetContentReady = false
|
||||
|
||||
private var updateTimestampTimer: SwiftSignalKit.Timer?
|
||||
|
||||
init(account: Account, tonContext: TonContext, presentationData: PresentationData, walletInfo: WalletInfo, address: String, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void, openTransaction: @escaping (WalletTransaction) -> Void) {
|
||||
self.account = account
|
||||
self.tonContext = tonContext
|
||||
@ -384,47 +509,50 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
||||
self.address = address
|
||||
self.openTransaction = openTransaction
|
||||
|
||||
self.headerNode = WalletInfoHeaderNode(theme: presentationData.theme, sendAction: sendAction, receiveAction: receiveAction)
|
||||
self.headerNode = WalletInfoHeaderNode(account: account, theme: presentationData.theme, sendAction: sendAction, receiveAction: receiveAction)
|
||||
|
||||
self.listNode = ListView()
|
||||
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
|
||||
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
|
||||
self.listNode.isHidden = true
|
||||
|
||||
self.loadingIndicator = UIActivityIndicatorView(style: .whiteLarge)
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = .white
|
||||
|
||||
self.balanceDisposable.set((getWalletState(address: address, tonInstance: tonContext.instance)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.headerNode.balanceNode.balance = formatBalanceText(max(0, value.balance))
|
||||
strongSelf.headerNode.balance = max(0, value.balance)
|
||||
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
|
||||
}
|
||||
|
||||
let wasReady = strongSelf.isReady
|
||||
strongSelf.isReady = strongSelf.headerNode.balance != nil && strongSelf.currentEntries != nil
|
||||
if strongSelf.isReady && !wasReady {
|
||||
strongSelf.animateReadyIn()
|
||||
}
|
||||
}))
|
||||
|
||||
self.addSubnode(self.listNode)
|
||||
self.addSubnode(self.headerNode)
|
||||
self.view.addSubview(self.loadingIndicator)
|
||||
|
||||
var canBeginRefresh = true
|
||||
var isScrolling = false
|
||||
self.listNode.beganInteractiveDragging = {
|
||||
isScrolling = true
|
||||
}
|
||||
self.listNode.endedInteractiveDragging = {
|
||||
isScrolling = false
|
||||
}
|
||||
|
||||
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in
|
||||
guard let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let headerHeight: CGFloat = navigationHeight + 260.0
|
||||
strongSelf.listOffset = offset
|
||||
|
||||
if strongSelf.isReady {
|
||||
if !strongSelf.reloadingState && canBeginRefresh && isScrolling {
|
||||
if offset >= headerHeight + 100.0 {
|
||||
canBeginRefresh = false
|
||||
strongSelf.headerNode.isRefreshing = true
|
||||
strongSelf.headerNode.animateBeganRefreshing()
|
||||
strongSelf.hapticFeedback.impact()
|
||||
strongSelf.refreshTransactions()
|
||||
}
|
||||
}
|
||||
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: offset, transition: listTransition)
|
||||
}
|
||||
}
|
||||
@ -436,12 +564,14 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
||||
guard case let .known(value) = offset, value < 100.0 else {
|
||||
return
|
||||
}
|
||||
if !strongSelf.loadingMoreTransactions && strongSelf.canLoadMoreTransactions {
|
||||
if !strongSelf.loadingMoreTransactions && !strongSelf.reloadingState && strongSelf.canLoadMoreTransactions {
|
||||
strongSelf.loadMoreTransactions()
|
||||
}
|
||||
}
|
||||
|
||||
self.listNode.didEndScrolling = { [weak self] in
|
||||
canBeginRefresh = true
|
||||
|
||||
guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else {
|
||||
return
|
||||
}
|
||||
@ -460,6 +590,26 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
||||
}
|
||||
|
||||
self.refreshTransactions()
|
||||
|
||||
self.updateTimestampTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in
|
||||
guard let strongSelf = self, let combinedState = strongSelf.combinedState, !strongSelf.reloadingState else {
|
||||
return
|
||||
}
|
||||
let string = lastUpdateTimestampString(strings: strongSelf.presentationData.strings, dateTimeFormat: strongSelf.presentationData.dateTimeFormat, statusTimestamp: Int32(clamping: combinedState.timestamp), relativeTo: Int32(Date().timeIntervalSince1970))
|
||||
if strongSelf.headerNode.timestampString != string {
|
||||
strongSelf.headerNode.timestampString = string
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: strongSelf.listOffset ?? 0.0, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}, queue: .mainQueue())
|
||||
self.updateTimestampTimer?.start()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stateDisposable.dispose()
|
||||
self.transactionListDisposable.dispose()
|
||||
self.updateTimestampTimer?.invalidate()
|
||||
}
|
||||
|
||||
func scrollToHideHeader() {
|
||||
@ -477,6 +627,9 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
||||
let isFirstLayout = self.validLayout == nil
|
||||
self.validLayout = (layout, navigationHeight)
|
||||
|
||||
let indicatorSize = self.loadingIndicator.bounds.size
|
||||
self.loadingIndicator.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: floor((layout.size.height - indicatorSize.height) / 2.0)), size: indicatorSize)
|
||||
|
||||
let headerHeight: CGFloat = navigationHeight + 260.0
|
||||
let topInset: CGFloat = headerHeight
|
||||
|
||||
@ -531,13 +684,73 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
||||
private func refreshTransactions() {
|
||||
self.transactionListDisposable.set(nil)
|
||||
self.loadingMoreTransactions = true
|
||||
self.reloadingState = true
|
||||
|
||||
self.transactionListDisposable.set((getWalletTransactions(address: self.address, previousId: nil, tonInstance: self.tonContext.instance)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] transactions in
|
||||
self.headerNode.timestampString = "updating"
|
||||
|
||||
self.stateDisposable.set((getCombinedWalletState(postbox: self.account.postbox, walletInfo: self.walletInfo, tonInstance: self.tonContext.instance)
|
||||
|> delay(self.combinedState == nil ? 0.0 : 2.0, queue: .mainQueue())
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.transactionsLoaded(isReload: true, transactions: transactions)
|
||||
let combinedState: CombinedWalletState?
|
||||
switch value {
|
||||
case let .cached(state):
|
||||
if state == nil {
|
||||
strongSelf.loadingIndicator.startAnimating()
|
||||
} else {
|
||||
strongSelf.loadingIndicator.stopAnimating()
|
||||
strongSelf.loadingIndicator.isHidden = true
|
||||
}
|
||||
combinedState = state
|
||||
case let .updated(state):
|
||||
strongSelf.loadingIndicator.stopAnimating()
|
||||
strongSelf.loadingIndicator.isHidden = true
|
||||
combinedState = state
|
||||
}
|
||||
|
||||
strongSelf.combinedState = combinedState
|
||||
if let combinedState = combinedState {
|
||||
strongSelf.headerNode.balanceNode.balance = formatBalanceText(max(0, combinedState.walletState.balance))
|
||||
strongSelf.headerNode.balance = max(0, combinedState.walletState.balance)
|
||||
|
||||
if strongSelf.isReady, let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
|
||||
}
|
||||
|
||||
strongSelf.reloadingState = false
|
||||
|
||||
strongSelf.headerNode.timestampString = lastUpdateTimestampString(strings: strongSelf.presentationData.strings, dateTimeFormat: strongSelf.presentationData.dateTimeFormat, statusTimestamp: Int32(clamping: combinedState.timestamp), relativeTo: Int32(Date().timeIntervalSince1970))
|
||||
|
||||
if strongSelf.isReady, let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: strongSelf.listOffset ?? 0.0, transition: .immediate)
|
||||
}
|
||||
|
||||
strongSelf.transactionsLoaded(isReload: true, transactions: combinedState.topTransactions)
|
||||
|
||||
strongSelf.headerNode.isRefreshing = false
|
||||
|
||||
if strongSelf.isReady, let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: strongSelf.listOffset ?? 0.0, transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
|
||||
let wasReady = strongSelf.isReady
|
||||
strongSelf.isReady = strongSelf.combinedState != nil
|
||||
|
||||
if strongSelf.isReady && !wasReady {
|
||||
if let (layout, navigationHeight) = strongSelf.validLayout {
|
||||
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: layout.size.height, transition: .immediate)
|
||||
}
|
||||
|
||||
strongSelf.becameReady(animated: strongSelf.didSetContentReady)
|
||||
}
|
||||
}
|
||||
|
||||
if !strongSelf.didSetContentReady {
|
||||
strongSelf.didSetContentReady = true
|
||||
strongSelf.contentReady.set(.single(true))
|
||||
}
|
||||
}, error: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -576,12 +789,14 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
||||
self.loadingMoreTransactions = false
|
||||
self.canLoadMoreTransactions = transactions.count > 2
|
||||
|
||||
let isFirst = self.currentEntries == nil
|
||||
|
||||
var updatedEntries: [WalletInfoListEntry] = []
|
||||
if isReload {
|
||||
var existingIds = Set<WalletTransactionId>()
|
||||
for transaction in transactions {
|
||||
updatedEntries.append(.transaction(updatedEntries.count, transaction))
|
||||
if !existingIds.contains(transaction.transactionId) {
|
||||
existingIds.insert(transaction.transactionId)
|
||||
updatedEntries.append(.transaction(updatedEntries.count, transaction))
|
||||
}
|
||||
}
|
||||
if updatedEntries.isEmpty {
|
||||
updatedEntries.append(.empty(self.address))
|
||||
@ -625,13 +840,6 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
||||
|
||||
self.enqueuedTransactions.append(transaction)
|
||||
self.dequeueTransaction()
|
||||
|
||||
let wasReady = self.isReady
|
||||
self.isReady = self.headerNode.balance != nil && self.currentEntries != nil
|
||||
|
||||
if self.isReady && !wasReady {
|
||||
self.animateReadyIn()
|
||||
}
|
||||
}
|
||||
|
||||
private func dequeueTransaction() {
|
||||
@ -650,11 +858,13 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
||||
})
|
||||
}
|
||||
|
||||
private func animateReadyIn() {
|
||||
private func becameReady(animated: Bool) {
|
||||
self.listNode.isHidden = false
|
||||
self.headerNode.animateIn()
|
||||
self.loadingIndicator.stopAnimating()
|
||||
self.loadingIndicator.isHidden = true
|
||||
self.headerNode.becameReady(animated: animated)
|
||||
if let (layout, navigationHeight) = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.5, curve: .spring))
|
||||
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,16 @@ class WalletInfoTransactionItem: ListViewItem {
|
||||
|
||||
private let titleFont = Font.medium(17.0)
|
||||
private let textFont = Font.monospace(15.0)
|
||||
private let descriptionFont = Font.regular(15.0)
|
||||
private let dateFont = Font.regular(14.0)
|
||||
private let directionFont = Font.regular(15.0)
|
||||
|
||||
private func formatAddress(_ address: String) -> String {
|
||||
var address = address
|
||||
address.insert("\n", at: address.index(address.startIndex, offsetBy: address.count / 2))
|
||||
return address
|
||||
}
|
||||
|
||||
class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let topStripeNode: ASDisplayNode
|
||||
@ -93,6 +100,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||
private let directionNode: TextNode
|
||||
private let iconNode: ASImageNode
|
||||
private let textNode: TextNode
|
||||
private let descriptionNode: TextNode
|
||||
private let dateNode: TextNode
|
||||
|
||||
private let activateArea: AccessibilityAreaNode
|
||||
@ -127,6 +135,11 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||
self.textNode.contentMode = .left
|
||||
self.textNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.descriptionNode = TextNode()
|
||||
self.descriptionNode.isUserInteractionEnabled = false
|
||||
self.descriptionNode.contentMode = .left
|
||||
self.descriptionNode.contentsScale = UIScreen.main.scale
|
||||
|
||||
self.dateNode = TextNode()
|
||||
self.dateNode.isUserInteractionEnabled = false
|
||||
self.dateNode.contentMode = .left
|
||||
@ -143,6 +156,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.directionNode)
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.descriptionNode)
|
||||
self.addSubnode(self.dateNode)
|
||||
|
||||
self.addSubnode(self.activateArea)
|
||||
@ -152,6 +166,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
let makeDirectionLayout = TextNode.asyncLayout(self.directionNode)
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode)
|
||||
let makeDateLayout = TextNode.asyncLayout(self.dateNode)
|
||||
|
||||
let currentItem = self.item
|
||||
@ -172,6 +187,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||
let titleColor: UIColor
|
||||
let transferredValue = item.walletTransaction.transferredValue
|
||||
var text: String = ""
|
||||
var description: String = ""
|
||||
if transferredValue <= 0 {
|
||||
title = "\(formatBalanceText(transferredValue))"
|
||||
titleColor = item.theme.list.itemPrimaryTextColor
|
||||
@ -184,7 +200,12 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||
if !text.isEmpty {
|
||||
text.append("\n")
|
||||
}
|
||||
text.append(message.destination)
|
||||
text.append(formatAddress(message.destination))
|
||||
|
||||
if !description.isEmpty {
|
||||
description.append("\n")
|
||||
}
|
||||
description.append(message.textMessage)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -192,7 +213,8 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||
titleColor = item.theme.chatList.secretTitleColor
|
||||
directionText = "from"
|
||||
if let inMessage = item.walletTransaction.inMessage {
|
||||
text = inMessage.source
|
||||
text = formatAddress(inMessage.source)
|
||||
description = inMessage.textMessage
|
||||
} else {
|
||||
text = "<unknown>"
|
||||
}
|
||||
@ -208,7 +230,9 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||
|
||||
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let contentSize: CGSize
|
||||
let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: description, font: descriptionFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
var contentSize: CGSize
|
||||
var insets: UIEdgeInsets
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
||||
@ -221,8 +245,12 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||
let topInset: CGFloat = 11.0
|
||||
let bottomInset: CGFloat = 11.0
|
||||
let titleSpacing: CGFloat = 2.0
|
||||
let textSpacing: CGFloat = 2.0
|
||||
|
||||
contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + titleSpacing + textLayout.size.height + bottomInset)
|
||||
if !descriptionLayout.size.width.isZero {
|
||||
contentSize.height += descriptionLayout.size.height + textSpacing
|
||||
}
|
||||
insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
var topHighlightInset: CGFloat = 0.0
|
||||
if dateHeaderAtBottom, let header = item.header {
|
||||
@ -249,6 +277,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||
|
||||
let _ = titleApply()
|
||||
let _ = textApply()
|
||||
let _ = descriptionApply()
|
||||
let _ = dateApply()
|
||||
let _ = directionApply()
|
||||
|
||||
@ -272,7 +301,11 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||
let directionFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + 3.0, y: titleFrame.maxY - directionLayout.size.height - 1.0), size: directionLayout.size)
|
||||
strongSelf.directionNode.frame = directionFrame
|
||||
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: textLayout.size)
|
||||
let textFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: textLayout.size)
|
||||
strongSelf.textNode.frame = textFrame
|
||||
|
||||
strongSelf.descriptionNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textFrame.maxY + textSpacing), size: descriptionLayout.size)
|
||||
|
||||
strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - dateLayout.size.width, y: topInset), size: dateLayout.size)
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: topHighlightInset + -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel * 2.0 - topHighlightInset))
|
||||
|
@ -12,6 +12,12 @@ import AnimationUI
|
||||
import SwiftSignalKit
|
||||
import OverlayStatusController
|
||||
import ItemListUI
|
||||
import TextFormat
|
||||
|
||||
public enum WalletSecureStorageResetReason {
|
||||
case notAvailable
|
||||
case changed
|
||||
}
|
||||
|
||||
public enum WalletSplashMode {
|
||||
case intro
|
||||
@ -20,6 +26,8 @@ public enum WalletSplashMode {
|
||||
case restoreFailed
|
||||
case sending(WalletInfo, String, Int64, String)
|
||||
case sent(WalletInfo, Int64)
|
||||
case secureStorageNotAvailable
|
||||
case secureStorageReset(WalletSecureStorageResetReason)
|
||||
}
|
||||
|
||||
public final class WalletSplashScreen: ViewController {
|
||||
@ -75,7 +83,7 @@ public final class WalletSplashScreen: ViewController {
|
||||
})
|
||||
case .sent:
|
||||
self.navigationItem.setLeftBarButton(UIBarButtonItem(customDisplayNode: ASDisplayNode())!, animated: false)
|
||||
case .created, .success, .restoreFailed:
|
||||
case .created, .success, .restoreFailed, .secureStorageNotAvailable, .secureStorageReset:
|
||||
break
|
||||
}
|
||||
|
||||
@ -110,6 +118,15 @@ public final class WalletSplashScreen: ViewController {
|
||||
}
|
||||
controller.dismiss()
|
||||
(strongSelf.navigationController as? NavigationController)?.replaceController(strongSelf, with: WalletSplashScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .created(walletInfo, wordList)), animated: true)
|
||||
}, error: { _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
controller.dismiss()
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: strongSelf.presentationData.theme), title: "An Error Occurred", text: "Sorry. Please try again.", actions: [
|
||||
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {
|
||||
})
|
||||
], actionLayout: .vertical), in: .window(.root))
|
||||
})
|
||||
case let .created(walletInfo, wordList):
|
||||
strongSelf.push(WalletWordDisplayScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, walletInfo: walletInfo, wordList: wordList))
|
||||
@ -159,12 +176,43 @@ public final class WalletSplashScreen: ViewController {
|
||||
}
|
||||
case .sending:
|
||||
break
|
||||
case .secureStorageNotAvailable:
|
||||
strongSelf.dismiss()
|
||||
case .secureStorageReset:
|
||||
strongSelf.push(WalletWordCheckScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .import))
|
||||
}
|
||||
}, secondaryAction: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.push(WalletWordCheckScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .import))
|
||||
switch strongSelf.mode {
|
||||
case .secureStorageNotAvailable, .secureStorageReset:
|
||||
if let navigationController = strongSelf.navigationController as? NavigationController {
|
||||
var controllers = navigationController.viewControllers
|
||||
controllers = controllers.filter { controller in
|
||||
if controller is WalletSplashScreen {
|
||||
return false
|
||||
}
|
||||
if controller is WalletWordDisplayScreen {
|
||||
return false
|
||||
}
|
||||
if controller is WalletWordCheckScreen {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
controllers.append(WalletSplashScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .intro))
|
||||
strongSelf.view.endEditing(true)
|
||||
navigationController.setViewControllers(controllers, animated: true)
|
||||
}
|
||||
default:
|
||||
strongSelf.push(WalletWordCheckScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .import))
|
||||
}
|
||||
}, openTerms: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: "https://telegram.org", forceExternal: true, presentationData: strongSelf.presentationData, navigationController: strongSelf.navigationController as? NavigationController, dismissInput: {})
|
||||
})
|
||||
|
||||
self.displayNodeDidLoad()
|
||||
@ -198,7 +246,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
||||
}
|
||||
}
|
||||
|
||||
init(account: Account, presentationData: PresentationData, mode: WalletSplashMode, action: @escaping () -> Void, secondaryAction: @escaping () -> Void) {
|
||||
init(account: Account, presentationData: PresentationData, mode: WalletSplashMode, action: @escaping () -> Void, secondaryAction: @escaping () -> Void, openTerms: @escaping () -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.mode = mode
|
||||
self.secondaryAction = secondaryAction
|
||||
@ -212,7 +260,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
||||
let title: String
|
||||
let text: String
|
||||
let buttonText: String
|
||||
let termsText: String
|
||||
let termsText: NSAttributedString
|
||||
let secondaryActionText: String
|
||||
|
||||
switch mode {
|
||||
@ -220,21 +268,23 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
||||
title = "Gram Wallet"
|
||||
text = "Gram wallet allows you to make fast and secure blockchain-based payments without intermediaries."
|
||||
buttonText = "Create My Wallet"
|
||||
termsText = "By creating the wallet you accept\nTerms of Conditions."
|
||||
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor, additionalAttributes: [:])
|
||||
let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor, additionalAttributes: [NSAttributedString.Key.underlineStyle.rawValue: NSUnderlineStyle.single.rawValue as NSNumber])
|
||||
termsText = parseMarkdownIntoAttributedString("By creating the wallet you accept\n[Terms of Conditions]().", attributes: MarkdownAttributes(body: body, bold: body, link: link, linkAttribute: { _ in nil }), textAlignment: .center)
|
||||
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/IntroIcon")
|
||||
secondaryActionText = ""
|
||||
case .created:
|
||||
title = "Congratulations"
|
||||
text = "Your Gram wallet has just been created. Only you control it.\n\nTo be able to always have access to it, please write down secret words and\nset up a secure passcode."
|
||||
buttonText = "Proceed"
|
||||
termsText = ""
|
||||
termsText = NSAttributedString(string: "")
|
||||
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/CreatedIcon")
|
||||
secondaryActionText = ""
|
||||
case .success:
|
||||
title = "Ready to go!"
|
||||
text = "You’re all set. Now you have a wallet that only you control - directly, without middlemen or bankers. "
|
||||
buttonText = "View My Wallet"
|
||||
termsText = ""
|
||||
termsText = NSAttributedString(string: "")
|
||||
self.iconNode.image = nil
|
||||
if let path = getAppBundle().path(forResource: "celebrate", ofType: "tgs") {
|
||||
self.animationNode.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
|
||||
@ -245,7 +295,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
||||
title = "Too Bad"
|
||||
text = "Without the secret words, you can't'nrestore access to the wallet."
|
||||
buttonText = "Create a New Wallet"
|
||||
termsText = ""
|
||||
termsText = NSAttributedString(string: "")
|
||||
self.iconNode.image = nil
|
||||
if let path = getAppBundle().path(forResource: "sad", ofType: "tgs") {
|
||||
self.animationNode.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
|
||||
@ -256,20 +306,47 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
||||
title = "Sending Grams"
|
||||
text = "Please wait a few seconds for your transaction to be processed..."
|
||||
buttonText = ""
|
||||
termsText = ""
|
||||
termsText = NSAttributedString(string: "")
|
||||
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/SendingIcon")
|
||||
secondaryActionText = ""
|
||||
case let .sent(_, amount):
|
||||
title = "Done!"
|
||||
text = "\(amount) Grams have been sent."
|
||||
buttonText = "View My Wallet"
|
||||
termsText = ""
|
||||
termsText = NSAttributedString(string: "")
|
||||
self.iconNode.image = nil
|
||||
if let path = getAppBundle().path(forResource: "celebrate", ofType: "tgs") {
|
||||
self.animationNode.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
secondaryActionText = ""
|
||||
case .secureStorageNotAvailable:
|
||||
title = "Too Bad"
|
||||
text = "Please set up Passcode on your device to enable secure payments with your Gram wallet."
|
||||
buttonText = "OK"
|
||||
termsText = NSAttributedString(string: "")
|
||||
self.iconNode.image = nil
|
||||
if let path = getAppBundle().path(forResource: "sad", ofType: "tgs") {
|
||||
self.animationNode.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
secondaryActionText = ""
|
||||
case let .secureStorageReset(reason):
|
||||
title = "Too Bad"
|
||||
switch reason {
|
||||
case .notAvailable:
|
||||
text = "Unfortunately, your wallet is no longer available because your system Passcode or Touch ID has been turned off."
|
||||
case .changed:
|
||||
text = "Unfortunately, your wallet is no longer available due to the change in your system security settings (Passcode/Touch ID). To restore your wallet, tap \"import existing wallet\"."
|
||||
}
|
||||
buttonText = "Import Existing Wallet"
|
||||
termsText = NSAttributedString(string: "")
|
||||
self.iconNode.image = nil
|
||||
if let path = getAppBundle().path(forResource: "sad", ofType: "tgs") {
|
||||
self.animationNode.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
|
||||
self.animationNode.visibility = true
|
||||
}
|
||||
secondaryActionText = "Create New Wallet"
|
||||
}
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
@ -287,7 +364,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
||||
|
||||
self.termsNode = ImmediateTextNode()
|
||||
self.termsNode.displaysAsynchronously = false
|
||||
self.termsNode.attributedText = NSAttributedString(string: termsText, font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
|
||||
self.termsNode.attributedText = termsText
|
||||
self.termsNode.maximumNumberOfLines = 0
|
||||
self.termsNode.textAlignment = .center
|
||||
|
||||
@ -331,6 +408,20 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
||||
}
|
||||
|
||||
self.secondaryActionButtonNode.addTarget(self, action: #selector(self.secondaryActionPressed), forControlEvents: .touchUpInside)
|
||||
|
||||
self.termsNode.linkHighlightColor = self.presentationData.theme.list.itemSecondaryTextColor.withAlphaComponent(0.5)
|
||||
self.termsNode.highlightAttributeAction = { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key.underlineStyle] {
|
||||
return NSAttributedString.Key.underlineStyle
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
self.termsNode.tapAttributeAction = { attributes in
|
||||
if let _ = attributes[NSAttributedString.Key.underlineStyle] {
|
||||
openTerms()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func secondaryActionPressed() {
|
||||
|
@ -27,6 +27,7 @@ private final class WalletTransactionInfoControllerArguments {
|
||||
private enum WalletTransactionInfoSection: Int32 {
|
||||
case amount
|
||||
case info
|
||||
case comment
|
||||
}
|
||||
|
||||
private enum WalletTransactionInfoEntry: ItemListNodeEntry {
|
||||
@ -35,6 +36,8 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
|
||||
case infoAddress(PresentationTheme, String)
|
||||
case infoCopyAddress(PresentationTheme, String)
|
||||
case infoSendGrams(PresentationTheme, String)
|
||||
case commentHeader(PresentationTheme, String)
|
||||
case comment(PresentationTheme, String)
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
@ -42,6 +45,8 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
|
||||
return WalletTransactionInfoSection.amount.rawValue
|
||||
case .infoHeader, .infoAddress, .infoCopyAddress, .infoSendGrams:
|
||||
return WalletTransactionInfoSection.info.rawValue
|
||||
case .commentHeader, .comment:
|
||||
return WalletTransactionInfoSection.comment.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +62,10 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
|
||||
return 3
|
||||
case .infoSendGrams:
|
||||
return 4
|
||||
case .commentHeader:
|
||||
return 5
|
||||
case .comment:
|
||||
return 6
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,6 +89,10 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
|
||||
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
arguments.sendGrams()
|
||||
})
|
||||
case let .commentHeader(theme, text):
|
||||
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
|
||||
case let .comment(theme, text):
|
||||
return ItemListMultilineTextItem(theme: theme, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -111,6 +124,30 @@ private func extractAddress(_ walletTransaction: WalletTransaction) -> String {
|
||||
return text
|
||||
}
|
||||
|
||||
private func extractDescription(_ walletTransaction: WalletTransaction) -> String {
|
||||
let transferredValue = walletTransaction.transferredValue
|
||||
var text = ""
|
||||
if transferredValue <= 0 {
|
||||
for message in walletTransaction.outMessages {
|
||||
if !text.isEmpty {
|
||||
text.append("\n\n")
|
||||
}
|
||||
text.append(message.textMessage)
|
||||
}
|
||||
} else {
|
||||
if let inMessage = walletTransaction.inMessage {
|
||||
text = inMessage.textMessage
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
private func formatAddress(_ address: String) -> String {
|
||||
var address = address
|
||||
address.insert("\n", at: address.index(address.startIndex, offsetBy: address.count / 2))
|
||||
return address
|
||||
}
|
||||
|
||||
private func walletTransactionInfoControllerEntries(presentationData: PresentationData, walletTransaction: WalletTransaction, state: WalletTransactionInfoControllerState) -> [WalletTransactionInfoEntry] {
|
||||
var entries: [WalletTransactionInfoEntry] = []
|
||||
|
||||
@ -118,16 +155,22 @@ private func walletTransactionInfoControllerEntries(presentationData: Presentati
|
||||
|
||||
let transferredValue = walletTransaction.transferredValue
|
||||
let text = extractAddress(walletTransaction)
|
||||
let description = extractDescription(walletTransaction)
|
||||
|
||||
if transferredValue <= 0 {
|
||||
entries.append(.infoHeader(presentationData.theme, "RECIPIENT"))
|
||||
} else {
|
||||
entries.append(.infoHeader(presentationData.theme, "SENDER"))
|
||||
}
|
||||
entries.append(.infoAddress(presentationData.theme, text))
|
||||
entries.append(.infoAddress(presentationData.theme, formatAddress(text)))
|
||||
entries.append(.infoCopyAddress(presentationData.theme, "Copy Address"))
|
||||
entries.append(.infoSendGrams(presentationData.theme, "Send Grams"))
|
||||
|
||||
if !description.isEmpty {
|
||||
entries.append(.commentHeader(presentationData.theme, "COMMENT"))
|
||||
entries.append(.comment(presentationData.theme, description))
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
@ -295,6 +338,8 @@ private class WalletTransactionHeaderItemNode: ListViewItemNode {
|
||||
contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset)
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
||||
|
||||
let titleScale: CGFloat = min(1.0, (params.width - 40.0 - iconSize.width) / titleLayout.size.width)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
return (layout, { [weak self] in
|
||||
@ -311,9 +356,11 @@ private class WalletTransactionHeaderItemNode: ListViewItemNode {
|
||||
let contentWidth = titleLayout.size.width + iconSpacing + iconSize.width / 2.0
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((params.width - contentWidth) / 2.0), y: verticalInset), size: titleLayout.size)
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: floor((params.width - subtitleLayout.size.width) / 2.0), y: titleFrame.maxY - 5.0), size: subtitleLayout.size)
|
||||
strongSelf.titleNode.frame = titleFrame
|
||||
strongSelf.titleNode.position = titleFrame.center
|
||||
strongSelf.titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
strongSelf.titleNode.transform = CATransform3DMakeScale(titleScale, titleScale, 1.0)
|
||||
strongSelf.subtitleNode.frame = subtitleFrame
|
||||
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + iconSpacing, y: titleFrame.minY + floor((titleFrame.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: floor(titleFrame.midX + titleFrame.width / 2.0 * titleScale + iconSpacing), y: titleFrame.minY + floor((titleFrame.height - iconSize.height) / 2.0) - 2.0), size: iconSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -327,4 +374,3 @@ private class WalletTransactionHeaderItemNode: ListViewItemNode {
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ import SolidRoundedButtonNode
|
||||
import UndoUI
|
||||
import AlertUI
|
||||
import SwiftSignalKit
|
||||
import TextFormat
|
||||
|
||||
private let possibleWordList: [String] = [
|
||||
"abandon",
|
||||
@ -2063,10 +2064,8 @@ private let possibleWordList: [String] = [
|
||||
"zoo"
|
||||
]
|
||||
|
||||
private let verifyWordIndices: [Int] = [4, 16, 21]
|
||||
|
||||
public enum WalletWordCheckMode {
|
||||
case verify(WalletInfo, [String])
|
||||
case verify(WalletInfo, [String], [Int])
|
||||
case `import`
|
||||
}
|
||||
|
||||
@ -2113,11 +2112,11 @@ public final class WalletWordCheckScreen: ViewController {
|
||||
return
|
||||
}
|
||||
switch strongSelf.mode {
|
||||
case let .verify(walletInfo, wordList):
|
||||
case let .verify(walletInfo, wordList, indices):
|
||||
let enteredWords = (strongSelf.displayNode as! WalletWordCheckScreenNode).enteredWords
|
||||
var isCorrect = true
|
||||
for i in 0 ..< enteredWords.count {
|
||||
if enteredWords[i] != wordList[verifyWordIndices[i]] {
|
||||
if enteredWords[i] != wordList[indices[i]] {
|
||||
isCorrect = false
|
||||
break
|
||||
}
|
||||
@ -2580,23 +2579,27 @@ private final class WalletWordCheckScreenNode: ViewControllerTracingNode, UIScro
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
|
||||
let title: String
|
||||
let text: String
|
||||
let text: NSAttributedString
|
||||
let buttonText: String
|
||||
let secondaryActionText: String
|
||||
|
||||
let wordIndices: [Int]
|
||||
|
||||
switch mode {
|
||||
case .verify:
|
||||
wordIndices = verifyWordIndices
|
||||
case let .verify(_, _, indices):
|
||||
wordIndices = indices
|
||||
title = "Test Time!"
|
||||
text = "Let’s check that you wrote them down correctly. Please enter words\n\(wordIndices[0] + 1), \(wordIndices[1] + 1) and \(wordIndices[2] + 1) below:"
|
||||
|
||||
let body = MarkdownAttributeSet(font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor, additionalAttributes: [:])
|
||||
let bold = MarkdownAttributeSet(font: Font.bold(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor, additionalAttributes: [NSAttributedString.Key.underlineStyle.rawValue: NSUnderlineStyle.single.rawValue as NSNumber])
|
||||
text = parseMarkdownIntoAttributedString("Let’s check that you wrote them down correctly. Please enter words\n**\(wordIndices[0] + 1)**, **\(wordIndices[1] + 1)** and **\(wordIndices[2] + 1)** below:", attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil }), textAlignment: .center)
|
||||
|
||||
buttonText = "Continue"
|
||||
secondaryActionText = ""
|
||||
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/WordsCheckIcon")
|
||||
case .import:
|
||||
title = "24 Secret Words"
|
||||
text = "Please restore access to your wallet by\nentering the 24 secret words you wrote down when creating the wallet."
|
||||
text = NSAttributedString(string: "Please restore access to your wallet by\nentering the 24 secret words you wrote down when creating the wallet.", font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||
buttonText = "Continue"
|
||||
secondaryActionText = "I don't have those"
|
||||
wordIndices = Array(0 ..< 24)
|
||||
@ -2617,7 +2620,7 @@ private final class WalletWordCheckScreenNode: ViewControllerTracingNode, UIScro
|
||||
|
||||
self.textNode = ImmediateTextNode()
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
||||
self.textNode.attributedText = text
|
||||
self.textNode.maximumNumberOfLines = 0
|
||||
self.textNode.lineSpacing = 0.1
|
||||
self.textNode.textAlignment = .center
|
||||
|
@ -72,7 +72,15 @@ public final class WalletWordDisplayScreen: ViewController {
|
||||
}
|
||||
})]), in: .window(.root))
|
||||
} else {
|
||||
strongSelf.push(WalletWordCheckScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .verify(strongSelf.walletInfo, strongSelf.wordList)))
|
||||
var wordIndices: [Int] = []
|
||||
while wordIndices.count < 3 {
|
||||
let index = Int(arc4random_uniform(UInt32(strongSelf.wordList.count)))
|
||||
if !wordIndices.contains(index) {
|
||||
wordIndices.append(index)
|
||||
}
|
||||
}
|
||||
wordIndices.sort()
|
||||
strongSelf.push(WalletWordCheckScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .verify(strongSelf.walletInfo, strongSelf.wordList, wordIndices)))
|
||||
}
|
||||
})
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user