mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-16 19:30:29 +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 \
|
//:IntentsExtension#dwarf-and-dsym,iphoneos-arm64 \
|
||||||
--verbose 8 ${BUCK_OPTIONS} ${BUCK_THREADS_OPTIONS} ${BUCK_DEBUG_OPTIONS}
|
--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
|
build_openssl: check_env
|
||||||
$(BUCK) build \
|
$(BUCK) build \
|
||||||
//submodules/openssl:openssl#iphoneos-arm64 \
|
//submodules/openssl:openssl#iphoneos-arm64 \
|
||||||
|
|||||||
@ -236,11 +236,11 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
|||||||
|> take(1)
|
|> take(1)
|
||||||
|> mapToSignal { matchedContacts in
|
|> mapToSignal { matchedContacts in
|
||||||
return account
|
return account
|
||||||
|> introduceError(IntentContactsError.self)
|
|> castError(IntentContactsError.self)
|
||||||
|> mapToSignal { account -> Signal<[(String, TelegramUser)], IntentContactsError> in
|
|> mapToSignal { account -> Signal<[(String, TelegramUser)], IntentContactsError> in
|
||||||
if let account = account {
|
if let account = account {
|
||||||
return matchingCloudContacts(postbox: account.postbox, contacts: matchedContacts)
|
return matchingCloudContacts(postbox: account.postbox, contacts: matchedContacts)
|
||||||
|> introduceError(IntentContactsError.self)
|
|> castError(IntentContactsError.self)
|
||||||
} else {
|
} else {
|
||||||
return .fail(.generic)
|
return .fail(.generic)
|
||||||
}
|
}
|
||||||
@ -278,11 +278,11 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
|||||||
let account = self.accountPromise.get()
|
let account = self.accountPromise.get()
|
||||||
|
|
||||||
let signal = account
|
let signal = account
|
||||||
|> introduceError(IntentHandlingError.self)
|
|> castError(IntentHandlingError.self)
|
||||||
|> mapToSignal { account -> Signal<INPerson?, IntentHandlingError> in
|
|> mapToSignal { account -> Signal<INPerson?, IntentHandlingError> in
|
||||||
if let account = account {
|
if let account = account {
|
||||||
return matchingCloudContact(postbox: account.postbox, peerId: PeerId(peerId))
|
return matchingCloudContact(postbox: account.postbox, peerId: PeerId(peerId))
|
||||||
|> introduceError(IntentHandlingError.self)
|
|> castError(IntentHandlingError.self)
|
||||||
|> map { user -> INPerson? in
|
|> map { user -> INPerson? in
|
||||||
if let user = user {
|
if let user = user {
|
||||||
return personWithUser(stableId: "tg\(peerId)", 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) {
|
func handle(intent: INSearchForMessagesIntent, completion: @escaping (INSearchForMessagesIntentResponse) -> Void) {
|
||||||
self.actionDisposable.set((self.accountPromise.get()
|
self.actionDisposable.set((self.accountPromise.get()
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> introduceError(IntentHandlingError.self)
|
|> castError(IntentHandlingError.self)
|
||||||
|> mapToSignal { account -> Signal<[INMessage], IntentHandlingError> in
|
|> mapToSignal { account -> Signal<[INMessage], IntentHandlingError> in
|
||||||
guard let account = account else {
|
guard let account = account else {
|
||||||
return .fail(.generic)
|
return .fail(.generic)
|
||||||
@ -409,7 +409,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (completion |> timeout(4.0, queue: Queue.mainQueue(), alternate: .single(Void())))
|
return (completion |> timeout(4.0, queue: Queue.mainQueue(), alternate: .single(Void())))
|
||||||
|> introduceError(IntentHandlingError.self)
|
|> castError(IntentHandlingError.self)
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> mapToSignal { _ -> Signal<[INMessage], IntentHandlingError> in
|
|> mapToSignal { _ -> Signal<[INMessage], IntentHandlingError> in
|
||||||
let messages: Signal<[INMessage], NoError>
|
let messages: Signal<[INMessage], NoError>
|
||||||
@ -419,7 +419,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
|||||||
messages = unreadMessages(account: account)
|
messages = unreadMessages(account: account)
|
||||||
}
|
}
|
||||||
return messages
|
return messages
|
||||||
|> introduceError(IntentHandlingError.self)
|
|> castError(IntentHandlingError.self)
|
||||||
|> afterDisposed {
|
|> afterDisposed {
|
||||||
account.shouldBeServiceTaskMaster.set(.single(.never))
|
account.shouldBeServiceTaskMaster.set(.single(.never))
|
||||||
}
|
}
|
||||||
@ -484,7 +484,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
|||||||
|
|
||||||
for (_, messageId) in maxMessageIdsToApply {
|
for (_, messageId) in maxMessageIdsToApply {
|
||||||
signals.append(applyMaxReadIndexInteractively(postbox: account.postbox, stateManager: account.stateManager, index: MessageIndex(id: messageId, timestamp: 0))
|
signals.append(applyMaxReadIndexInteractively(postbox: account.postbox, stateManager: account.stateManager, index: MessageIndex(id: messageId, timestamp: 0))
|
||||||
|> introduceError(IntentHandlingError.self))
|
|> castError(IntentHandlingError.self))
|
||||||
}
|
}
|
||||||
|
|
||||||
if signals.isEmpty {
|
if signals.isEmpty {
|
||||||
@ -576,7 +576,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
|||||||
func handle(intent: INSearchCallHistoryIntent, completion: @escaping (INSearchCallHistoryIntentResponse) -> Void) {
|
func handle(intent: INSearchCallHistoryIntent, completion: @escaping (INSearchCallHistoryIntentResponse) -> Void) {
|
||||||
self.actionDisposable.set((self.accountPromise.get()
|
self.actionDisposable.set((self.accountPromise.get()
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> introduceError(IntentHandlingError.self)
|
|> castError(IntentHandlingError.self)
|
||||||
|> mapToSignal { account -> Signal<[CallRecord], IntentHandlingError> in
|
|> mapToSignal { account -> Signal<[CallRecord], IntentHandlingError> in
|
||||||
guard let account = account else {
|
guard let account = account else {
|
||||||
return .fail(.generic)
|
return .fail(.generic)
|
||||||
@ -584,7 +584,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
|||||||
|
|
||||||
account.shouldBeServiceTaskMaster.set(.single(.now))
|
account.shouldBeServiceTaskMaster.set(.single(.now))
|
||||||
return missedCalls(account: account)
|
return missedCalls(account: account)
|
||||||
|> introduceError(IntentHandlingError.self)
|
|> castError(IntentHandlingError.self)
|
||||||
|> afterDisposed {
|
|> afterDisposed {
|
||||||
account.shouldBeServiceTaskMaster.set(.single(.never))
|
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";
|
"Channel.AdminLog.CanDeleteMessagesOfOthers" = "Delete Messages of Others";
|
||||||
|
|
||||||
"ChatSearch.ResultsTooltip" = "Tap to view as a list.";
|
"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;
|
+ (DeviceSpecificEncryptionParameters * _Nonnull)deviceSpecificEncryptionParameters:(NSString * _Nonnull)rootPath baseAppBundleId:(NSString * _Nonnull)baseAppBundleId;
|
||||||
- (NSData * _Nullable)bundleDataWithAppToken:(NSData * _Nullable)appToken;
|
- (NSData * _Nullable)bundleDataWithAppToken:(NSData * _Nullable)appToken;
|
||||||
|
|
||||||
+ (void)encryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion;
|
+ (void)getHardwareEncryptionAvailableWithBaseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion;
|
||||||
+ (void)decryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(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
|
@end
|
||||||
|
|||||||
@ -16,6 +16,8 @@
|
|||||||
#import <MtProtoKitDynamic/MtProtoKitDynamic.h>
|
#import <MtProtoKitDynamic/MtProtoKitDynamic.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static NSString *telegramApplicationSecretKey = @"telegramApplicationSecretKey_v3";
|
||||||
|
|
||||||
static uint32_t funcSwap32(uint32_t input)
|
static uint32_t funcSwap32(uint32_t input)
|
||||||
{
|
{
|
||||||
return OSSwapBigToHostInt32(input);
|
return OSSwapBigToHostInt32(input);
|
||||||
@ -238,7 +240,7 @@ API_AVAILABLE(ios(10))
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (NSData * _Nullable)encrypt:(NSData * _Nonnull)data;
|
- (NSData * _Nullable)encrypt:(NSData * _Nonnull)data;
|
||||||
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data;
|
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data cancelled:(bool *)cancelled;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@ -258,6 +260,11 @@ API_AVAILABLE(ios(10))
|
|||||||
CFRelease(_publicKey);
|
CFRelease(_publicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSData * _Nullable)getPublicKey {
|
||||||
|
NSData *result = CFBridgingRelease(SecKeyCopyExternalRepresentation(_publicKey, nil));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
- (NSData * _Nullable)encrypt:(NSData * _Nonnull)data {
|
- (NSData * _Nullable)encrypt:(NSData * _Nonnull)data {
|
||||||
if (data.length % 16 != 0) {
|
if (data.length % 16 != 0) {
|
||||||
return nil;
|
return nil;
|
||||||
@ -274,12 +281,17 @@ API_AVAILABLE(ios(10))
|
|||||||
return cipherText;
|
return cipherText;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data {
|
- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data cancelled:(bool *)cancelled {
|
||||||
CFErrorRef error = NULL;
|
CFErrorRef error = NULL;
|
||||||
NSData *plainText = (NSData *)CFBridgingRelease(SecKeyCreateDecryptedData(_privateKey, kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM, (__bridge CFDataRef)data, &error));
|
NSData *plainText = (NSData *)CFBridgingRelease(SecKeyCreateDecryptedData(_privateKey, kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM, (__bridge CFDataRef)data, &error));
|
||||||
|
|
||||||
if (!plainText) {
|
if (!plainText) {
|
||||||
__unused NSError *err = CFBridgingRelease(error);
|
__unused NSError *err = CFBridgingRelease(error);
|
||||||
|
if (err.code == -2) {
|
||||||
|
if (cancelled) {
|
||||||
|
*cancelled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,22 +443,29 @@ API_AVAILABLE(ios(10))
|
|||||||
return bundleSeedID;
|
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];
|
NSString *bundleSeedId = [self bundleSeedId];
|
||||||
if (bundleSeedId == nil) {
|
if (bundleSeedId == nil) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NSData *applicationTag = [self applicationSecretTag:isCheckKey];
|
||||||
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
|
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
|
||||||
|
|
||||||
NSData *applicationTag = [@"telegramLocalKey" dataUsingEncoding:NSUTF8StringEncoding];
|
|
||||||
|
|
||||||
NSDictionary *query = @{
|
NSDictionary *query = @{
|
||||||
(id)kSecClass: (id)kSecClassKey,
|
(id)kSecClass: (id)kSecClassKey,
|
||||||
(id)kSecAttrApplicationTag: applicationTag,
|
(id)kSecAttrApplicationTag: applicationTag,
|
||||||
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
|
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
|
||||||
(id)kSecAttrAccessGroup: (id)accessGroup,
|
(id)kSecAttrAccessGroup: (id)accessGroup,
|
||||||
(id)kSecReturnRef: @YES,
|
(id)kSecReturnRef: @YES
|
||||||
};
|
};
|
||||||
SecKeyRef privateKey = NULL;
|
SecKeyRef privateKey = NULL;
|
||||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey);
|
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&privateKey);
|
||||||
@ -474,13 +493,13 @@ API_AVAILABLE(ios(10))
|
|||||||
return result;
|
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];
|
NSString *bundleSeedId = [self bundleSeedId];
|
||||||
if (bundleSeedId == nil) {
|
if (bundleSeedId == nil) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSData *applicationTag = [@"telegramLocalKey" dataUsingEncoding:NSUTF8StringEncoding];
|
NSData *applicationTag = [self applicationSecretTag:isCheckKey];
|
||||||
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
|
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
|
||||||
|
|
||||||
NSDictionary *query = @{
|
NSDictionary *query = @{
|
||||||
@ -496,142 +515,21 @@ API_AVAILABLE(ios(10))
|
|||||||
return true;
|
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];
|
NSString *bundleSeedId = [self bundleSeedId];
|
||||||
if (bundleSeedId == nil) {
|
if (bundleSeedId == nil) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSData *applicationTag = [@"telegramLocalKey" dataUsingEncoding:NSUTF8StringEncoding];
|
NSData *applicationTag = [self applicationSecretTag:isCheckKey];
|
||||||
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
|
NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId];
|
||||||
|
|
||||||
SecAccessControlRef access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleAlwaysThisDeviceOnly, kSecAccessControlPrivateKeyUsage, NULL);
|
SecAccessControlRef access;
|
||||||
NSDictionary *attributes = @{
|
if (isCheckKey) {
|
||||||
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
|
access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlPrivateKeyUsage, NULL);
|
||||||
(id)kSecAttrKeySizeInBits: @256,
|
} else {
|
||||||
(id)kSecAttrTokenID: (id)kSecAttrTokenIDSecureEnclave,
|
access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlUserPresence | kSecAccessControlPrivateKeyUsage, NULL);
|
||||||
(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = @{
|
NSDictionary *attributes = @{
|
||||||
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
|
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
|
||||||
(id)kSecAttrKeySizeInBits: @256,
|
(id)kSecAttrKeySizeInBits: @256,
|
||||||
@ -738,24 +636,67 @@ API_AVAILABLE(ios(10))
|
|||||||
return [[DeviceSpecificEncryptionParameters alloc] initWithKey:key salt:salt];
|
return [[DeviceSpecificEncryptionParameters alloc] initWithKey:key salt:salt];
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (void)encryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion {
|
+ (dispatch_queue_t)encryptionQueue {
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
static dispatch_queue_t instance = nil;
|
||||||
LocalPrivateKey *privateKey = [self getApplicationSecretKey:baseAppBundleId];
|
static dispatch_once_t onceToken;
|
||||||
if (privateKey == nil) {
|
dispatch_once(&onceToken, ^{
|
||||||
privateKey = [self addApplicationSecretKey:baseAppBundleId];
|
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];
|
||||||
}
|
}
|
||||||
if (privateKey == nil) {
|
|
||||||
completion(nil);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
NSData *result = [privateKey encrypt:secret];
|
} else {
|
||||||
completion(result);
|
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:false];
|
||||||
|
[self removeApplicationSecretKey:baseAppBundleId isCheckKey:true];
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
completion([privateKey getPublicKey]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (void)decryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable))completion {
|
+ (void)encryptApplicationSecret:(NSData * _Nonnull)secret baseAppBundleId:(NSString * _Nonnull)baseAppBundleId completion:(void (^)(NSData * _Nullable, NSData * _Nullable))completion {
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
dispatch_async([self encryptionQueue], ^{
|
||||||
LocalPrivateKey *privateKey = [self getApplicationSecretKey:baseAppBundleId];
|
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) {
|
if (privateKey == nil) {
|
||||||
completion(nil);
|
completion(nil);
|
||||||
return;
|
return;
|
||||||
@ -764,7 +705,16 @@ API_AVAILABLE(ios(10))
|
|||||||
completion(nil);
|
completion(nil);
|
||||||
return;
|
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);
|
completion(result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -242,6 +242,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
public final var visibleContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in }
|
public final var visibleContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in }
|
||||||
public final var visibleBottomContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in }
|
public final var visibleBottomContentOffsetChanged: (ListViewVisibleContentOffset) -> Void = { _ in }
|
||||||
public final var beganInteractiveDragging: () -> Void = { }
|
public final var beganInteractiveDragging: () -> Void = { }
|
||||||
|
public final var endedInteractiveDragging: () -> Void = { }
|
||||||
public final var didEndScrolling: (() -> Void)?
|
public final var didEndScrolling: (() -> Void)?
|
||||||
|
|
||||||
private var currentGeneralScrollDirection: GeneralScrollDirection?
|
private var currentGeneralScrollDirection: GeneralScrollDirection?
|
||||||
@ -599,6 +600,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
|
|||||||
self.lastContentOffsetTimestamp = 0.0
|
self.lastContentOffsetTimestamp = 0.0
|
||||||
self.didEndScrolling?()
|
self.didEndScrolling?()
|
||||||
}
|
}
|
||||||
|
self.endedInteractiveDragging()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||||
|
|||||||
@ -86,7 +86,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var statusBarStyle: StatusBarStyle = .Ignore
|
var statusBarStyle: StatusBarStyle = .Ignore
|
||||||
var statusBarStyleUpdated: (() -> Void)?
|
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||||
|
|
||||||
init(controllerRemoved: @escaping (ViewController) -> Void) {
|
init(controllerRemoved: @escaping (ViewController) -> Void) {
|
||||||
self.controllerRemoved = controllerRemoved
|
self.controllerRemoved = controllerRemoved
|
||||||
@ -260,7 +260,7 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
} else {
|
} else {
|
||||||
transitionType = .pop
|
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)
|
self?.pendingChildIsReady(pendingChild)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -268,12 +268,16 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var statusBarTransition = transition
|
||||||
|
|
||||||
if let pending = self.state.pending {
|
if let pending = self.state.pending {
|
||||||
if pending.isReady {
|
if pending.isReady {
|
||||||
self.state.pending = nil
|
self.state.pending = nil
|
||||||
let previous = self.state.top
|
let previous = self.state.top
|
||||||
|
previous?.value.view.endEditing(true)
|
||||||
self.state.top = pending.value
|
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 {
|
if !self.isReady {
|
||||||
self.isReady = true
|
self.isReady = true
|
||||||
self.isReadyUpdated?()
|
self.isReadyUpdated?()
|
||||||
@ -287,11 +291,16 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
self.topTransition(from: previous, to: nil, transitionType: .pop, layout: layout, transition: .immediate)
|
self.topTransition(from: previous, to: nil, transitionType: .pop, layout: layout, transition: .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var updatedStatusBarStyle = self.statusBarStyle
|
||||||
if let top = self.state.top {
|
if let top = self.state.top {
|
||||||
self.applyLayout(layout: layout, to: top, transition: transition)
|
self.applyLayout(layout: layout, to: top, transition: transition)
|
||||||
self.statusBarStyle = top.value.statusBar.statusBarStyle
|
updatedStatusBarStyle = top.value.statusBar.statusBarStyle
|
||||||
} else {
|
} else {
|
||||||
self.statusBarStyle = .Ignore
|
updatedStatusBarStyle = .Ignore
|
||||||
|
}
|
||||||
|
if self.statusBarStyle != updatedStatusBarStyle {
|
||||||
|
self.statusBarStyle = updatedStatusBarStyle
|
||||||
|
self.statusBarStyleUpdated?(statusBarTransition)
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.state.transition == nil {
|
if self.state.transition == nil {
|
||||||
|
|||||||
@ -368,6 +368,12 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
let flatContainer = NavigationContainer(controllerRemoved: { [weak self] controller in
|
let flatContainer = NavigationContainer(controllerRemoved: { [weak self] controller in
|
||||||
self?.controllerRemoved(controller)
|
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.displayNode.insertSubnode(flatContainer, at: 0)
|
||||||
self.rootContainer = .flat(flatContainer)
|
self.rootContainer = .flat(flatContainer)
|
||||||
flatContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
|
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
|
let flatContainer = NavigationContainer(controllerRemoved: { [weak self] controller in
|
||||||
self?.controllerRemoved(controller)
|
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.displayNode.insertSubnode(flatContainer, at: 0)
|
||||||
self.rootContainer = .flat(flatContainer)
|
self.rootContainer = .flat(flatContainer)
|
||||||
flatContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
|
flatContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||||
@ -394,6 +406,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||||||
self.rootContainer = .split(splitContainer)
|
self.rootContainer = .split(splitContainer)
|
||||||
splitContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
|
splitContainer.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||||
splitContainer.update(layout: layout, masterControllers: masterControllers, detailControllers: detailControllers, transition: .immediate)
|
splitContainer.update(layout: layout, masterControllers: masterControllers, detailControllers: detailControllers, transition: .immediate)
|
||||||
|
flatContainer.statusBarStyleUpdated = nil
|
||||||
flatContainer.removeFromSupernode()
|
flatContainer.removeFromSupernode()
|
||||||
case let .split(splitContainer):
|
case let .split(splitContainer):
|
||||||
transition.updateFrame(node: splitContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
|
transition.updateFrame(node: splitContainer, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||||
|
|||||||
@ -34,7 +34,7 @@ private func importedAccountData(basePath: String, documentsPath: String, accoun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|> ignoreValues
|
|> ignoreValues
|
||||||
|> introduceError(AccountImportError.self)
|
|> castError(AccountImportError.self)
|
||||||
|
|
||||||
let importData = importPreferencesData(documentsPath: documentsPath, masterDatacenterId: Int32(masterDatacenterId), account: account, database: database)
|
let importData = importPreferencesData(documentsPath: documentsPath, masterDatacenterId: Int32(masterDatacenterId), account: account, database: database)
|
||||||
|> mapToSignal { accountUserId -> Signal<(AccountImportProgressType, Float), AccountImportError> in
|
|> 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))
|
transaction.setState(AuthorizedAccountState(isTestingEnvironment: false, masterDatacenterId: masterDatacenterId, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: parsedAccountUserId), state: nil))
|
||||||
return parsedAccountUserId
|
return parsedAccountUserId
|
||||||
}
|
}
|
||||||
|> introduceError(AccountImportError.self)
|
|> castError(AccountImportError.self)
|
||||||
} else {
|
} else {
|
||||||
return .fail(.generic)
|
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 })
|
transaction.updatePeerPresencesInternal(presences: [user.id: presence], merge: { _, updated in return updated })
|
||||||
}
|
}
|
||||||
|> ignoreValues
|
|> ignoreValues
|
||||||
|> introduceError(AccountImportError.self)
|
|> castError(AccountImportError.self)
|
||||||
}
|
}
|
||||||
|
|
||||||
let importedSecretChats = loadLegacySecretChats(account: account, basePath: basePath, accountPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: accountUserId), database: database)
|
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)
|
/*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)
|
let importedLegacyPreferences = importLegacyPreferences(accountManager: accountManager, account: account, documentsPath: basePath + "/Documents", database: database)
|
||||||
|> introduceError(AccountImportError.self)
|
|> castError(AccountImportError.self)
|
||||||
|
|
||||||
return importedAccountUser
|
return importedAccountUser
|
||||||
|> map { _ -> (AccountImportProgressType, Float) in return (.generic, 0.0) }
|
|> 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)
|
return temporaryAccount(manager: accountManager, rootPath: rootPathForBasePath(basePath), encryptionParameters: encryptionParameters)
|
||||||
|> introduceError(AccountImportError.self)
|
|> castError(AccountImportError.self)
|
||||||
|> mapToSignal { account -> Signal<ImportedLegacyAccountEvent, AccountImportError> in
|
|> mapToSignal { account -> Signal<ImportedLegacyAccountEvent, AccountImportError> in
|
||||||
let actions = importedAccountData(basePath: basePath, documentsPath: documentsPath, accountManager: accountManager, account: account, database: database)
|
let actions = importedAccountData(basePath: basePath, documentsPath: documentsPath, accountManager: accountManager, account: account, database: database)
|
||||||
var result = actions
|
var result = actions
|
||||||
|
|||||||
@ -209,7 +209,7 @@ public final class SecureIdAuthController: ViewController {
|
|||||||
|
|
||||||
switch self.mode {
|
switch self.mode {
|
||||||
case let .form(peerId, scope, publicKey, callbackUrl, _, _):
|
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
|
|> mapToSignal { form, configuration -> Signal<SecureIdEncryptedFormData, RequestSecureIdFormError> in
|
||||||
return context.account.postbox.transaction { transaction -> 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 {
|
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)
|
handleError(error, callbackUrl, peerId)
|
||||||
})
|
})
|
||||||
case .list:
|
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 {
|
guard let accountPeer = transaction.getPeer(context.account.peerId) else {
|
||||||
return .fail(.generic)
|
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)
|
return updateGroupDiscussionForChannel(network: context.account.network, postbox: context.account.postbox, channelId: peerId, groupId: resultPeerId)
|
||||||
}
|
}
|
||||||
|> introduceError(ChannelDiscussionGroupError.self)
|
|> castError(ChannelDiscussionGroupError.self)
|
||||||
|> switchToLatest
|
|> switchToLatest
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -355,7 +355,7 @@ public func channelMembersController(context: AccountContext, peerId: PeerId) ->
|
|||||||
|
|
||||||
addMembersDisposable.set((contactsController.result
|
addMembersDisposable.set((contactsController.result
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|> introduceError(AddChannelMemberError.self)
|
|> castError(AddChannelMemberError.self)
|
||||||
|> mapToSignal { [weak contactsController] contacts -> Signal<Never, AddChannelMemberError> in
|
|> mapToSignal { [weak contactsController] contacts -> Signal<Never, AddChannelMemberError> in
|
||||||
contactsController?.displayProgress = true
|
contactsController?.displayProgress = true
|
||||||
|
|
||||||
|
|||||||
@ -1105,7 +1105,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _ = (contactDataManager.createContactWithData(composedContactData)
|
let _ = (contactDataManager.createContactWithData(composedContactData)
|
||||||
|> introduceError(AddContactError.self)
|
|> castError(AddContactError.self)
|
||||||
|> mapToSignal { contactIdAndData -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, AddContactError> in
|
|> mapToSignal { contactIdAndData -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, AddContactError> in
|
||||||
guard let (id, data) = contactIdAndData else {
|
guard let (id, data) = contactIdAndData else {
|
||||||
return .single(nil)
|
return .single(nil)
|
||||||
@ -1122,7 +1122,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device
|
|||||||
context.account.postbox.transaction { transaction -> (DeviceContactStableId, DeviceContactExtendedData, Peer?)? in
|
context.account.postbox.transaction { transaction -> (DeviceContactStableId, DeviceContactExtendedData, Peer?)? in
|
||||||
return (id, data, transaction.getPeer(peer.id))
|
return (id, data, transaction.getPeer(peer.id))
|
||||||
}
|
}
|
||||||
|> introduceError(AddContactError.self)
|
|> castError(AddContactError.self)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
default:
|
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)
|
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
|
|> mapToSignal { peerId -> Signal<(DeviceContactStableId, DeviceContactExtendedData, Peer?)?, AddContactError> in
|
||||||
if let peerId = peerId {
|
if let peerId = peerId {
|
||||||
return context.account.postbox.transaction { transaction -> (DeviceContactStableId, DeviceContactExtendedData, Peer?)? in
|
return context.account.postbox.transaction { transaction -> (DeviceContactStableId, DeviceContactExtendedData, Peer?)? in
|
||||||
return (id, data, transaction.getPeer(peerId))
|
return (id, data, transaction.getPeer(peerId))
|
||||||
}
|
}
|
||||||
|> introduceError(AddContactError.self)
|
|> castError(AddContactError.self)
|
||||||
} else {
|
} else {
|
||||||
return .single((id, data, nil))
|
return .single((id, data, nil))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1760,7 +1760,7 @@ public func chatMessagePhotoInteractiveFetched(context: AccountContext, photoRef
|
|||||||
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||||
if case .remote = type, let peerType = storeToDownloadsPeerType {
|
if case .remote = type, let peerType = storeToDownloadsPeerType {
|
||||||
return storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: photoReference.abstract, peerType: peerType)
|
return storeDownloadedMedia(storeManager: context.downloadedMediaStoreManager, media: photoReference.abstract, peerType: peerType)
|
||||||
|> introduceError(FetchResourceError.self)
|
|> castError(FetchResourceError.self)
|
||||||
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
|
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||||
return .complete()
|
return .complete()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,6 +54,8 @@
|
|||||||
BOOL _isOpenGLLoaded;
|
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;
|
- (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);
|
@property (nonatomic, copy) void (^startMessaging)(void);
|
||||||
|
|||||||
@ -71,6 +71,23 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
|
|||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
@interface RMIntroView : UIView
|
||||||
|
|
||||||
|
@property (nonatomic, copy) void (^onLayout)();
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation RMIntroView
|
||||||
|
|
||||||
|
- (void)layoutSubviews {
|
||||||
|
[super layoutSubviews];
|
||||||
|
|
||||||
|
if (_onLayout) {
|
||||||
|
_onLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
@interface RMIntroViewController () <UIGestureRecognizerDelegate>
|
@interface RMIntroViewController () <UIGestureRecognizerDelegate>
|
||||||
{
|
{
|
||||||
@ -92,6 +109,8 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
|
|||||||
|
|
||||||
SVariable *_alternativeLocalization;
|
SVariable *_alternativeLocalization;
|
||||||
NSDictionary<NSString *, NSString *> *_englishStrings;
|
NSDictionary<NSString *, NSString *> *_englishStrings;
|
||||||
|
|
||||||
|
bool _loadedView;
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@ -215,12 +234,6 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void)loadView
|
|
||||||
{
|
|
||||||
[super loadView];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)loadGL
|
- (void)loadGL
|
||||||
{
|
{
|
||||||
if (/*[[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground*/true && !_isOpenGLLoaded)
|
if (/*[[UIApplication sharedApplication] applicationState] != UIApplicationStateBackground*/true && !_isOpenGLLoaded)
|
||||||
@ -273,10 +286,28 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
|
|||||||
_isOpenGLLoaded = false;
|
_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
|
- (void)viewDidLoad
|
||||||
{
|
{
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
|
|
||||||
|
if (_loadedView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_loadedView = true;
|
||||||
|
|
||||||
self.view.backgroundColor = _backgroundColor;
|
self.view.backgroundColor = _backgroundColor;
|
||||||
|
|
||||||
[self loadGL];
|
[self loadGL];
|
||||||
@ -411,7 +442,7 @@ static void TGDispatchOnMainThread(dispatch_block_t block) {
|
|||||||
return deviceScreen;
|
return deviceScreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)viewWillLayoutSubviews
|
- (void)updateLayout
|
||||||
{
|
{
|
||||||
UIInterfaceOrientation isVertical = (self.view.bounds.size.height / self.view.bounds.size.width > 1.0f);
|
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 {
|
if !wallets.wallets.isEmpty {
|
||||||
let _ = (testGiverWalletAddress(tonInstance: tonContext.instance)
|
let _ = (testGiverWalletAddress(tonInstance: tonContext.instance)
|
||||||
|> deliverOnMainQueue).start(next: { address in
|
|> 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
|
|> deliverOnMainQueue).start(next: { wallets in
|
||||||
if let tonContext = context.tonContext {
|
if let tonContext = context.tonContext {
|
||||||
if !wallets.wallets.isEmpty {
|
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
|
|> deliverOnMainQueue).start(next: { address in
|
||||||
let _ = (getGramsFromTestGiver(address: address, amount: 1500000000, tonInstance: tonContext.instance)
|
let _ = (getGramsFromTestGiver(address: address, amount: 1500000000, tonInstance: tonContext.instance)
|
||||||
|> deliverOnMainQueue).start(completed: {
|
|> deliverOnMainQueue).start(completed: {
|
||||||
|
|||||||
@ -1609,16 +1609,33 @@ private func accountContextMenuItems(context: AccountContext, logout: @escaping
|
|||||||
}
|
}
|
||||||
|
|
||||||
func openWallet(context: AccountContext, push: @escaping (ViewController) -> Void) {
|
func openWallet(context: AccountContext, push: @escaping (ViewController) -> Void) {
|
||||||
let _ = (availableWallets(postbox: context.account.postbox)
|
guard let tonContext = context.tonContext else {
|
||||||
|> deliverOnMainQueue).start(next: { wallets in
|
return
|
||||||
if let tonContext = context.tonContext {
|
}
|
||||||
|
let _ = (combineLatest(queue: .mainQueue(),
|
||||||
|
availableWallets(postbox: context.account.postbox),
|
||||||
|
tonContext.keychain.encryptionPublicKey()
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue).start(next: { wallets, currentPublicKey in
|
||||||
if wallets.wallets.isEmpty {
|
if wallets.wallets.isEmpty {
|
||||||
|
if let _ = currentPublicKey {
|
||||||
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .intro))
|
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .intro))
|
||||||
} else {
|
} else {
|
||||||
let _ = (walletAddress(publicKey: wallets.wallets[0].publicKey, tonInstance: tonContext.instance)
|
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
|
|> deliverOnMainQueue).start(next: { address in
|
||||||
push(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: wallets.wallets[0], address: address))
|
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
|
dataSignals = dataSignals
|
||||||
|> then(
|
|> then(
|
||||||
wrappedSignal
|
wrappedSignal
|
||||||
|> introduceError(Void.self)
|
|> castError(Void.self)
|
||||||
|> take(1)
|
|> take(1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -339,11 +339,11 @@ public func sentShareItems(account: Account, to peerIds: [PeerId], items: [Prepa
|
|||||||
}
|
}
|
||||||
|
|
||||||
return enqueueMessagesToMultiplePeers(account: account, peerIds: peerIds, messages: messages)
|
return enqueueMessagesToMultiplePeers(account: account, peerIds: peerIds, messages: messages)
|
||||||
|> introduceError(Void.self)
|
|> castError(Void.self)
|
||||||
|> mapToSignal { messageIds -> Signal<Float, Void> in
|
|> mapToSignal { messageIds -> Signal<Float, Void> in
|
||||||
let key: PostboxViewKey = .messages(Set(messageIds))
|
let key: PostboxViewKey = .messages(Set(messageIds))
|
||||||
return account.postbox.combinedView(keys: [key])
|
return account.postbox.combinedView(keys: [key])
|
||||||
|> introduceError(Void.self)
|
|> castError(Void.self)
|
||||||
|> mapToSignal { view -> Signal<Float, Void> in
|
|> mapToSignal { view -> Signal<Float, Void> in
|
||||||
if let messagesView = view.views[key] as? MessagesView {
|
if let messagesView = view.views[key] as? MessagesView {
|
||||||
for (_, message) in messagesView.messages {
|
for (_, message) in messagesView.messages {
|
||||||
|
|||||||
@ -11,11 +11,32 @@ import MtProtoKit
|
|||||||
import TelegramApi
|
import TelegramApi
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public struct TonKeychain {
|
public struct TonKeychainEncryptedData: Codable, Equatable {
|
||||||
public let encrypt: (Data) -> Signal<Data?, NoError>
|
public let publicKey: Data
|
||||||
public let decrypt: (Data) -> Signal<Data?, NoError>
|
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.encrypt = encrypt
|
||||||
self.decrypt = decrypt
|
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
|
return Signal { subscriber in
|
||||||
let disposable = MetaDisposable()
|
let disposable = MetaDisposable()
|
||||||
self.impl.with { impl in
|
self.impl.with { impl in
|
||||||
@ -104,17 +125,14 @@ public final class TonInstance {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let cancel = keychain.encrypt(key.secret).start(next: { encryptedSecretData in
|
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
|
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()
|
subscriber.putCompletion()
|
||||||
}, error: { _ in
|
}, error: { error in
|
||||||
preconditionFailure()
|
subscriber.putError(.generic)
|
||||||
})
|
})
|
||||||
}, error: { _ in
|
}, error: { _ in
|
||||||
|
subscriber.putError(.generic)
|
||||||
}, completed: {
|
}, completed: {
|
||||||
})
|
})
|
||||||
}, error: { _ in
|
}, error: { _ in
|
||||||
@ -142,30 +160,14 @@ public final class TonInstance {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let cancel = keychain.encrypt(key.secret).start(next: { encryptedSecretData in
|
let cancel = keychain.encrypt(key.secret).start(next: { encryptedSecretData in
|
||||||
guard let encryptedSecretData = encryptedSecretData else {
|
subscriber.putNext(WalletInfo(publicKey: WalletPublicKey(rawValue: key.publicKey), encryptedSecret: encryptedSecretData))
|
||||||
subscriber.putError(.generic)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
subscriber.putNext(WalletInfo(publicKey: WalletPublicKey(rawValue: key.publicKey), encryptedSecret: EncryptedWalletSecret(rawValue: encryptedSecretData)))
|
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
}, error: { _ in
|
}, error: { _ in
|
||||||
|
subscriber.putError(.generic)
|
||||||
}, completed: {
|
}, completed: {
|
||||||
})
|
})
|
||||||
}, error: { error in
|
}, 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)
|
subscriber.putError(.generic)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
subscriber.putError(.generic)
|
|
||||||
}
|
|
||||||
}, completed: {
|
}, completed: {
|
||||||
})
|
})
|
||||||
disposable.set(ActionDisposable {
|
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)
|
return self.getWalletStateRaw(address: address)
|
||||||
|> map { state in
|
|> 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> {
|
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)
|
return keychain.decrypt(walletInfo.encryptedSecret)
|
||||||
|> castError(SendGramsFromWalletError.self)
|
|> mapError { _ -> SendGramsFromWalletError in
|
||||||
|> mapToSignal { decryptedSecret -> Signal<Never, SendGramsFromWalletError> in
|
return .secretDecryptionFailed
|
||||||
guard let decryptedSecret = decryptedSecret else {
|
|
||||||
return .fail(.secretDecryptionFailed)
|
|
||||||
}
|
}
|
||||||
|
|> mapToSignal { decryptedSecret -> Signal<Never, SendGramsFromWalletError> in
|
||||||
let key = TONKey(publicKey: walletInfo.publicKey.rawValue, secret: decryptedSecret)
|
let key = TONKey(publicKey: walletInfo.publicKey.rawValue, secret: decryptedSecret)
|
||||||
|
|
||||||
return self.ensureWalletInitialized(address: fromAddress, key: key, serverSalt: serverSalt)
|
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> {
|
fileprivate func walletRestoreWords(walletInfo: WalletInfo, keychain: TonKeychain, serverSalt: Data) -> Signal<[String], WalletRestoreWordsError> {
|
||||||
return keychain.decrypt(walletInfo.encryptedSecret.rawValue)
|
return keychain.decrypt(walletInfo.encryptedSecret)
|
||||||
|> castError(WalletRestoreWordsError.self)
|
|> mapError { _ -> WalletRestoreWordsError in
|
||||||
|> mapToSignal { decryptedSecret -> Signal<[String], WalletRestoreWordsError> in
|
return .secretDecryptionFailed
|
||||||
guard let decryptedSecret = decryptedSecret else {
|
|
||||||
return .fail(.secretDecryptionFailed)
|
|
||||||
}
|
}
|
||||||
|
|> mapToSignal { decryptedSecret -> Signal<[String], WalletRestoreWordsError> in
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
let disposable = MetaDisposable()
|
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 struct WalletInfo: PostboxCoding, Codable, Equatable {
|
||||||
public let publicKey: WalletPublicKey
|
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.publicKey = publicKey
|
||||||
self.encryptedSecret = encryptedSecret
|
self.encryptedSecret = encryptedSecret
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(decoder: PostboxDecoder) {
|
public init(decoder: PostboxDecoder) {
|
||||||
self.publicKey = WalletPublicKey(rawValue: decoder.decodeStringForKey("publicKey", orElse: ""))
|
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) {
|
public func encode(_ encoder: PostboxEncoder) {
|
||||||
encoder.encodeString(self.publicKey.rawValue, forKey: "publicKey")
|
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) {
|
public init(decoder: PostboxDecoder) {
|
||||||
self.info = decoder.decodeDataForKey("info").flatMap { data in
|
self.info = decoder.decodeDataForKey("info").flatMap { data in
|
||||||
return try? JSONDecoder().decode(WalletInfo.self, from: data)
|
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
|
self.state = decoder.decodeDataForKey("state").flatMap { data in
|
||||||
return try? JSONDecoder().decode(CombinedWalletState.self, from: data)
|
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
|
|> mapToSignal { serverSalt -> Signal<(WalletInfo, [String]), CreateWalletError> in
|
||||||
return tonInstance.createWallet(keychain: keychain, serverSalt: serverSalt)
|
return tonInstance.createWallet(keychain: keychain, serverSalt: serverSalt)
|
||||||
|> castError(CreateWalletError.self)
|
|
||||||
|> mapToSignal { walletInfo, wordList -> Signal<(WalletInfo, [String]), CreateWalletError> in
|
|> mapToSignal { walletInfo, wordList -> Signal<(WalletInfo, [String]), CreateWalletError> in
|
||||||
return postbox.transaction { transaction -> (WalletInfo, [String]) in
|
return postbox.transaction { transaction -> (WalletInfo, [String]) in
|
||||||
transaction.updatePreferencesEntry(key: PreferencesKeys.walletCollection, { current in
|
transaction.updatePreferencesEntry(key: PreferencesKeys.walletCollection, { current in
|
||||||
@ -634,7 +630,6 @@ public func createWallet(postbox: Postbox, network: Network, tonInstance: TonIns
|
|||||||
|
|
||||||
private enum ImportWalletInternalError {
|
private enum ImportWalletInternalError {
|
||||||
case generic
|
case generic
|
||||||
case fileExists(String)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ImportWalletError {
|
public enum ImportWalletError {
|
||||||
@ -652,12 +647,6 @@ public func importWallet(postbox: Postbox, network: Network, tonInstance: TonIns
|
|||||||
switch error {
|
switch error {
|
||||||
case .generic:
|
case .generic:
|
||||||
return .fail(.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
|
|> mapToSignal { walletInfo -> Signal<WalletInfo, ImportWalletError> in
|
||||||
@ -718,7 +707,7 @@ public func testGiverWalletAddress(tonInstance: TonInstance) -> Signal<String, N
|
|||||||
return tonInstance.testGiverWalletAddress()
|
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)
|
return tonInstance.getWalletState(address: address)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -750,7 +739,7 @@ public func getCombinedWalletState(postbox: Postbox, walletInfo: WalletInfo, ton
|
|||||||
|> mapToSignal { address -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
|> mapToSignal { address -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
||||||
return getWalletState(address: address, tonInstance: tonInstance)
|
return getWalletState(address: address, tonInstance: tonInstance)
|
||||||
|> castError(GetCombinedWalletStateError.self)
|
|> castError(GetCombinedWalletStateError.self)
|
||||||
|> mapToSignal { walletState -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
|> mapToSignal { walletState, syncUtime -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
||||||
let topTransactions: Signal<[WalletTransaction], GetCombinedWalletStateError>
|
let topTransactions: Signal<[WalletTransaction], GetCombinedWalletStateError>
|
||||||
if walletState.lastTransactionId == cachedState?.walletState.lastTransactionId {
|
if walletState.lastTransactionId == cachedState?.walletState.lastTransactionId {
|
||||||
topTransactions = .single(cachedState?.topTransactions ?? [])
|
topTransactions = .single(cachedState?.topTransactions ?? [])
|
||||||
@ -762,7 +751,7 @@ public func getCombinedWalletState(postbox: Postbox, walletInfo: WalletInfo, ton
|
|||||||
}
|
}
|
||||||
return topTransactions
|
return topTransactions
|
||||||
|> mapToSignal { topTransactions -> Signal<CombinedWalletStateResult, GetCombinedWalletStateError> in
|
|> 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
|
return postbox.transaction { transaction -> CombinedWalletStateResult in
|
||||||
transaction.updatePreferencesEntry(key: PreferencesKeys.walletCollection, { current in
|
transaction.updatePreferencesEntry(key: PreferencesKeys.walletCollection, { current in
|
||||||
var walletCollection = (current as? WalletCollection) ?? WalletCollection(wallets: [])
|
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
|
let tonKeychain: TonKeychain
|
||||||
|
|
||||||
#if targetEnvironment(simulator)
|
#if targetEnvironment(simulator)
|
||||||
tonKeychain = TonKeychain(encrypt: { data in
|
tonKeychain = TonKeychain(encryptionPublicKey: {
|
||||||
|
return .single(Data())
|
||||||
|
}, encrypt: { data in
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
subscriber.putNext(data)
|
subscriber.putNext(data)
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
@ -664,18 +666,34 @@ final class SharedApplicationContext {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
#else
|
#else
|
||||||
tonKeychain = TonKeychain(encrypt: { data in
|
tonKeychain = TonKeychain(encryptionPublicKey: {
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
BuildConfig.encryptApplicationSecret(data, baseAppBundleId: baseAppBundleId, completion: { result in
|
BuildConfig.getHardwareEncryptionAvailable(withBaseAppBundleId: baseAppBundleId, completion: { value in
|
||||||
subscriber.putNext(result)
|
subscriber.putNext(value)
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
})
|
})
|
||||||
return EmptyDisposable
|
return EmptyDisposable
|
||||||
}
|
}
|
||||||
}, decrypt: { data in
|
}, encrypt: { data in
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
BuildConfig.decryptApplicationSecret(data, baseAppBundleId: baseAppBundleId, completion: { result in
|
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)
|
subscriber.putNext(result)
|
||||||
|
} else {
|
||||||
|
subscriber.putError(.generic)
|
||||||
|
}
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
})
|
})
|
||||||
return EmptyDisposable
|
return EmptyDisposable
|
||||||
@ -1319,14 +1337,6 @@ final class SharedApplicationContext {
|
|||||||
self.isInForegroundPromise.set(true)
|
self.isInForegroundPromise.set(true)
|
||||||
self.isActiveValue = true
|
self.isActiveValue = true
|
||||||
self.isActivePromise.set(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) {
|
func applicationWillTerminate(_ application: UIApplication) {
|
||||||
|
|||||||
@ -22,6 +22,8 @@ final class AuthorizationSequenceSplashController: ViewController {
|
|||||||
|
|
||||||
private let controller: RMIntroViewController
|
private let controller: RMIntroViewController
|
||||||
|
|
||||||
|
private var validLayout: ContainerViewLayout?
|
||||||
|
|
||||||
var nextPressed: ((PresentationStrings?) -> Void)?
|
var nextPressed: ((PresentationStrings?) -> Void)?
|
||||||
|
|
||||||
private let suggestedLocalization = Promise<SuggestedLocalizationInfo?>()
|
private let suggestedLocalization = Promise<SuggestedLocalizationInfo?>()
|
||||||
@ -102,7 +104,9 @@ final class AuthorizationSequenceSplashController: ViewController {
|
|||||||
private func addControllerIfNeeded() {
|
private func addControllerIfNeeded() {
|
||||||
if !controller.isViewLoaded || controller.view.superview == nil {
|
if !controller.isViewLoaded || controller.view.superview == nil {
|
||||||
self.displayNode.view.addSubview(controller.view)
|
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)
|
controller.viewDidAppear(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,14 +138,18 @@ final class AuthorizationSequenceSplashController: ViewController {
|
|||||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||||
super.containerLayoutUpdated(layout, transition: transition)
|
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.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: transition)
|
||||||
|
|
||||||
self.addControllerIfNeeded()
|
self.addControllerIfNeeded()
|
||||||
if case .immediate = transition {
|
if case .immediate = transition {
|
||||||
self.controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
|
self.controller.view.frame = controllerFrame
|
||||||
} else {
|
} else {
|
||||||
UIView.animate(withDuration: 0.3, animations: {
|
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> {
|
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)
|
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
|
|> mapToSignal { update -> Signal<ChatHistoryViewUpdate, Bool> in
|
||||||
switch update {
|
switch update {
|
||||||
case let .Loading(value):
|
case let .Loading(value):
|
||||||
|
|||||||
@ -79,7 +79,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
|||||||
let stickerSettings: StickerSettings = (transaction.getSharedData(ApplicationSpecificSharedDataKeys.stickerSettings) as? StickerSettings) ?? .defaultSettings
|
let stickerSettings: StickerSettings = (transaction.getSharedData(ApplicationSpecificSharedDataKeys.stickerSettings) as? StickerSettings) ?? .defaultSettings
|
||||||
return stickerSettings
|
return stickerSettings
|
||||||
}
|
}
|
||||||
|> introduceError(ChatContextQueryError.self)
|
|> castError(ChatContextQueryError.self)
|
||||||
|> mapToSignal { stickerSettings -> Signal<[FoundStickerItem], ChatContextQueryError> in
|
|> mapToSignal { stickerSettings -> Signal<[FoundStickerItem], ChatContextQueryError> in
|
||||||
let scope: SearchStickersScope
|
let scope: SearchStickersScope
|
||||||
switch stickerSettings.emojiStickerSuggestionMode {
|
switch stickerSettings.emojiStickerSuggestionMode {
|
||||||
@ -91,7 +91,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
|||||||
scope = [.installed]
|
scope = [.installed]
|
||||||
}
|
}
|
||||||
return searchStickers(account: context.account, query: query.basicEmoji.0, scope: scope)
|
return searchStickers(account: context.account, query: query.basicEmoji.0, scope: scope)
|
||||||
|> introduceError(ChatContextQueryError.self)
|
|> castError(ChatContextQueryError.self)
|
||||||
}
|
}
|
||||||
|> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
|> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||||
return { _ in
|
return { _ in
|
||||||
@ -123,7 +123,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
|||||||
}
|
}
|
||||||
return { _ in return .hashtags(result) }
|
return { _ in return .hashtags(result) }
|
||||||
}
|
}
|
||||||
|> introduceError(ChatContextQueryError.self)
|
|> castError(ChatContextQueryError.self)
|
||||||
|
|
||||||
return signal |> then(hashtags)
|
return signal |> then(hashtags)
|
||||||
case let .mention(query, types):
|
case let .mention(query, types):
|
||||||
@ -178,7 +178,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
|||||||
}
|
}
|
||||||
return { _ in return .mentions(sortedPeers) }
|
return { _ in return .mentions(sortedPeers) }
|
||||||
}
|
}
|
||||||
|> introduceError(ChatContextQueryError.self)
|
|> castError(ChatContextQueryError.self)
|
||||||
|
|
||||||
return signal |> then(participants)
|
return signal |> then(participants)
|
||||||
case let .command(query):
|
case let .command(query):
|
||||||
@ -207,7 +207,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
|||||||
let sortedCommands = filteredCommands
|
let sortedCommands = filteredCommands
|
||||||
return { _ in return .commands(sortedCommands) }
|
return { _ in return .commands(sortedCommands) }
|
||||||
}
|
}
|
||||||
|> introduceError(ChatContextQueryError.self)
|
|> castError(ChatContextQueryError.self)
|
||||||
return signal |> then(commands)
|
return signal |> then(commands)
|
||||||
case let .contextRequest(addressName, query):
|
case let .contextRequest(addressName, query):
|
||||||
var delayRequest = true
|
var delayRequest = true
|
||||||
@ -239,7 +239,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
|
|||||||
return .single(nil)
|
return .single(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|> introduceError(ChatContextQueryError.self)
|
|> castError(ChatContextQueryError.self)
|
||||||
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> in
|
|> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> in
|
||||||
if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder {
|
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: "")
|
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
|
|> map { result -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||||
return { _ in return .emojis(result, range) }
|
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()
|
createSignal = addressPromise.get()
|
||||||
|> introduceError(CreateGroupError.self)
|
|> castError(CreateGroupError.self)
|
||||||
|> mapToSignal { address -> Signal<PeerId?, CreateGroupError> in
|
|> mapToSignal { address -> Signal<PeerId?, CreateGroupError> in
|
||||||
guard let address = address else {
|
guard let address = address else {
|
||||||
return .complete()
|
return .complete()
|
||||||
|
|||||||
@ -291,7 +291,7 @@ func fetchEmojiSpriteResource(postbox: Postbox, network: Network, resource: Emoj
|
|||||||
let packName = "P\(resource.packId)_by_AEStickerBot"
|
let packName = "P\(resource.packId)_by_AEStickerBot"
|
||||||
|
|
||||||
return loadedStickerPack(postbox: postbox, network: network, reference: .name(packName), forceActualized: false)
|
return loadedStickerPack(postbox: postbox, network: network, reference: .name(packName), forceActualized: false)
|
||||||
|> introduceError(MediaResourceDataFetchError.self)
|
|> castError(MediaResourceDataFetchError.self)
|
||||||
|> mapToSignal { result -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
|
|> mapToSignal { result -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
|
||||||
switch result {
|
switch result {
|
||||||
case let .result(_, items, _):
|
case let .result(_, items, _):
|
||||||
|
|||||||
@ -218,7 +218,7 @@ private final class FetchManagerCategoryContext {
|
|||||||
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||||
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
|
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
|
||||||
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|
||||||
|> introduceError(FetchResourceError.self)
|
|> castError(FetchResourceError.self)
|
||||||
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
|
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||||
return .complete()
|
return .complete()
|
||||||
}
|
}
|
||||||
@ -347,7 +347,7 @@ private final class FetchManagerCategoryContext {
|
|||||||
activeContext.disposable?.dispose()
|
activeContext.disposable?.dispose()
|
||||||
if isVideoPreload {
|
if isVideoPreload {
|
||||||
activeContext.disposable = (preloadVideoResource(postbox: self.postbox, resourceReference: entry.resourceReference, duration: 4.0)
|
activeContext.disposable = (preloadVideoResource(postbox: self.postbox, resourceReference: entry.resourceReference, duration: 4.0)
|
||||||
|> introduceError(FetchResourceError.self)
|
|> castError(FetchResourceError.self)
|
||||||
|> map { _ -> FetchResourceSourceType in return .local }
|
|> map { _ -> FetchResourceSourceType in return .local }
|
||||||
|> then(.single(.local))
|
|> then(.single(.local))
|
||||||
|> deliverOnMainQueue).start(next: { _ in
|
|> deliverOnMainQueue).start(next: { _ in
|
||||||
@ -359,7 +359,7 @@ private final class FetchManagerCategoryContext {
|
|||||||
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
|> mapToSignal { type -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||||
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
|
if let storeManager = storeManager, let mediaReference = entry.mediaReference, case .remote = type, let peerType = entry.storeToDownloadsPeerType {
|
||||||
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|
return storeDownloadedMedia(storeManager: storeManager, media: mediaReference, peerType: peerType)
|
||||||
|> introduceError(FetchResourceError.self)
|
|> castError(FetchResourceError.self)
|
||||||
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
|
|> mapToSignal { _ -> Signal<FetchResourceSourceType, FetchResourceError> in
|
||||||
return .complete()
|
return .complete()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -224,7 +224,7 @@ public func fetchVideoLibraryMediaResource(postbox: Postbox, resource: VideoLibr
|
|||||||
|> map { view in
|
|> map { view in
|
||||||
return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue
|
return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue
|
||||||
}
|
}
|
||||||
|> introduceError(MediaResourceDataFetchError.self)
|
|> castError(MediaResourceDataFetchError.self)
|
||||||
|> mapToSignal { appConfiguration -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
|
|> mapToSignal { appConfiguration -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
|
||||||
let config = VideoConversionConfiguration.with(appConfiguration: appConfiguration)
|
let config = VideoConversionConfiguration.with(appConfiguration: appConfiguration)
|
||||||
let signal = Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> { subscriber in
|
let signal = Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> { subscriber in
|
||||||
@ -343,7 +343,7 @@ func fetchLocalFileVideoMediaResource(postbox: Postbox, resource: LocalFileVideo
|
|||||||
|> map { view in
|
|> map { view in
|
||||||
return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue
|
return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue
|
||||||
}
|
}
|
||||||
|> introduceError(MediaResourceDataFetchError.self)
|
|> castError(MediaResourceDataFetchError.self)
|
||||||
|> mapToSignal { appConfiguration -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
|
|> mapToSignal { appConfiguration -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> in
|
||||||
let config = VideoConversionConfiguration.with(appConfiguration: appConfiguration)
|
let config = VideoConversionConfiguration.with(appConfiguration: appConfiguration)
|
||||||
let signal = Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> { subscriber in
|
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
|
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)
|
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
|
|> mapToSignal { sharedContext, loggingSettings -> Signal<(SharedAccountContextImpl, Account, [AccountWithInfo]), ShareAuthorizationError> in
|
||||||
Logger.shared.logToFile = loggingSettings.logToFile
|
Logger.shared.logToFile = loggingSettings.logToFile
|
||||||
Logger.shared.logToConsole = loggingSettings.logToConsole
|
Logger.shared.logToConsole = loggingSettings.logToConsole
|
||||||
@ -187,7 +187,7 @@ public class ShareRootControllerImpl {
|
|||||||
Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData
|
Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData
|
||||||
|
|
||||||
return sharedContext.activeAccountsWithInfo
|
return sharedContext.activeAccountsWithInfo
|
||||||
|> introduceError(ShareAuthorizationError.self)
|
|> castError(ShareAuthorizationError.self)
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> mapToSignal { primary, accounts -> Signal<(SharedAccountContextImpl, Account, [AccountWithInfo]), ShareAuthorizationError> in
|
|> mapToSignal { primary, accounts -> Signal<(SharedAccountContextImpl, Account, [AccountWithInfo]), ShareAuthorizationError> in
|
||||||
guard let primary = primary else {
|
guard let primary = primary else {
|
||||||
@ -209,7 +209,7 @@ public class ShareRootControllerImpl {
|
|||||||
return combineLatest(sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]), limitsConfiguration, sharedContext.accountManager.accessChallengeData())
|
return combineLatest(sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.presentationPasscodeSettings]), limitsConfiguration, sharedContext.accountManager.accessChallengeData())
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|> introduceError(ShareAuthorizationError.self)
|
|> castError(ShareAuthorizationError.self)
|
||||||
|> map { sharedData, limitsConfiguration, data -> (AccountContext, PostboxAccessChallengeData, [AccountWithInfo]) in
|
|> map { sharedData, limitsConfiguration, data -> (AccountContext, PostboxAccessChallengeData, [AccountWithInfo]) in
|
||||||
updateLegacyLocalization(strings: sharedContext.currentPresentationData.with({ $0 }).strings)
|
updateLegacyLocalization(strings: sharedContext.currentPresentationData.with({ $0 }).strings)
|
||||||
let context = AccountContextImpl(sharedContext: sharedContext, account: account, tonContext: nil, limitsConfiguration: limitsConfiguration)
|
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 }) {
|
if let videoBuffer = result?.buffers.videoBuffer, let impl = source.syncWith({ $0 }) {
|
||||||
return impl.ensureHasFrames(until: min(duration, videoBuffer.duration.seconds))
|
return impl.ensureHasFrames(until: min(duration, videoBuffer.duration.seconds))
|
||||||
|> ignoreValues
|
|> ignoreValues
|
||||||
|> introduceError(MediaFrameSourceSeekError.self)
|
|> castError(MediaFrameSourceSeekError.self)
|
||||||
} else {
|
} else {
|
||||||
return .complete()
|
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 (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
|
let contentVerticalOrigin: CGFloat = 32.0
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,59 @@ import SolidRoundedButtonNode
|
|||||||
import AnimationUI
|
import AnimationUI
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import MergeLists
|
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 {
|
public final class WalletInfoScreen: ViewController {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
@ -20,6 +73,11 @@ public final class WalletInfoScreen: ViewController {
|
|||||||
|
|
||||||
private var presentationData: PresentationData
|
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) {
|
public init(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, address: String) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.tonContext = tonContext
|
self.tonContext = tonContext
|
||||||
@ -77,6 +135,8 @@ public final class WalletInfoScreen: ViewController {
|
|||||||
})
|
})
|
||||||
|
|
||||||
self.displayNodeDidLoad()
|
self.displayNodeDidLoad()
|
||||||
|
|
||||||
|
self._ready.set((self.displayNode as! WalletInfoScreenNode).contentReady.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
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 balanceTextFrame = CGRect(origin: balanceOrigin, size: balanceTextSize)
|
||||||
let balanceIconFrame: CGRect
|
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.updateFrameAdditive(node: self.balanceTextNode, frame: balanceTextFrame)
|
||||||
transition.updateFrame(node: self.balanceIconNode, frame: balanceIconFrame)
|
transition.updateFrame(node: self.balanceIconNode, frame: balanceIconFrame)
|
||||||
|
|
||||||
@ -140,20 +196,33 @@ private final class WalletInfoBalanceNode: ASDisplayNode {
|
|||||||
|
|
||||||
private final class WalletInfoHeaderNode: ASDisplayNode {
|
private final class WalletInfoHeaderNode: ASDisplayNode {
|
||||||
var balance: Int64?
|
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
|
let balanceNode: WalletInfoBalanceNode
|
||||||
|
private let refreshNode: AnimatedStickerNode
|
||||||
private let balanceSubtitleNode: ImmediateTextNode
|
private let balanceSubtitleNode: ImmediateTextNode
|
||||||
|
private let balanceTimestampNode: ImmediateTextNode
|
||||||
private let receiveButtonNode: SolidRoundedButtonNode
|
private let receiveButtonNode: SolidRoundedButtonNode
|
||||||
private let sendButtonNode: SolidRoundedButtonNode
|
private let sendButtonNode: SolidRoundedButtonNode
|
||||||
private let headerBackgroundNode: ASImageNode
|
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.balanceNode = WalletInfoBalanceNode(theme: theme)
|
||||||
|
|
||||||
self.balanceSubtitleNode = ImmediateTextNode()
|
self.balanceSubtitleNode = ImmediateTextNode()
|
||||||
self.balanceSubtitleNode.displaysAsynchronously = false
|
self.balanceSubtitleNode.displaysAsynchronously = false
|
||||||
self.balanceSubtitleNode.attributedText = NSAttributedString(string: "your balance", font: Font.regular(13), textColor: UIColor(white: 1.0, alpha: 0.6))
|
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 = ASImageNode()
|
||||||
self.headerBackgroundNode.displaysAsynchronously = false
|
self.headerBackgroundNode.displaysAsynchronously = false
|
||||||
self.headerBackgroundNode.displayWithoutProcessing = true
|
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.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.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()
|
super.init()
|
||||||
|
|
||||||
self.addSubnode(self.headerBackgroundNode)
|
self.addSubnode(self.headerBackgroundNode)
|
||||||
@ -175,6 +251,8 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
|
|||||||
self.addSubnode(self.sendButtonNode)
|
self.addSubnode(self.sendButtonNode)
|
||||||
self.addSubnode(self.balanceNode)
|
self.addSubnode(self.balanceNode)
|
||||||
self.addSubnode(self.balanceSubtitleNode)
|
self.addSubnode(self.balanceSubtitleNode)
|
||||||
|
self.addSubnode(self.balanceTimestampNode)
|
||||||
|
self.addSubnode(self.refreshNode)
|
||||||
|
|
||||||
self.receiveButtonNode.pressed = {
|
self.receiveButtonNode.pressed = {
|
||||||
receiveAction()
|
receiveAction()
|
||||||
@ -206,11 +284,13 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
|
|||||||
let buttonAlpha = buttonTransition * 1.0
|
let buttonAlpha = buttonTransition * 1.0
|
||||||
|
|
||||||
let balanceSubtitleSize = self.balanceSubtitleNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: 200.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 balanceHeight = self.balanceNode.update(width: size.width, transition: transition)
|
||||||
let balanceSize = CGSize(width: size.width, height: balanceHeight)
|
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
|
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 headerScaleTransition: CGFloat = max(0.0, min(1.0, (effectiveOffset - minHeaderOffset) / (maxHeaderOffset - minHeaderOffset)))
|
||||||
let headerPositionTransition: CGFloat = max(0.0, (effectiveOffset - minHeaderOffset) / (maxOffset - minHeaderOffset))
|
let headerPositionTransition: CGFloat = max(0.0, (effectiveOffset - minHeaderOffset) / (maxOffset - minHeaderOffset))
|
||||||
let headerY = headerPositionTransition * maxHeaderY + (1.0 - headerPositionTransition) * minHeaderY
|
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)
|
let balanceFrame = CGRect(origin: CGPoint(x: 0.0, y: headerY), size: balanceSize)
|
||||||
transition.updateFrame(node: self.balanceNode, frame: balanceFrame)
|
transition.updateFrame(node: self.balanceNode, frame: balanceFrame)
|
||||||
transition.updateSublayerTransformScale(node: self.balanceNode, scale: headerScale)
|
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
|
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)))
|
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 {
|
if self.balance == nil {
|
||||||
self.balanceNode.balanceTextNode.isHidden = true
|
self.balanceNode.isHidden = true
|
||||||
self.balanceSubtitleNode.isHidden = true
|
self.balanceSubtitleNode.isHidden = true
|
||||||
|
self.balanceTimestampNode.isHidden = true
|
||||||
} else {
|
} else {
|
||||||
self.balanceNode.balanceTextNode.isHidden = false
|
self.balanceNode.isHidden = false
|
||||||
self.balanceSubtitleNode.isHidden = false
|
self.balanceSubtitleNode.isHidden = false
|
||||||
|
self.balanceTimestampNode.isHidden = false
|
||||||
}
|
}
|
||||||
transition.updateFrame(node: self.receiveButtonNode, frame: receiveButtonFrame)
|
transition.updateFrame(node: self.receiveButtonNode, frame: receiveButtonFrame)
|
||||||
transition.updateAlpha(node: self.receiveButtonNode, alpha: buttonAlpha)
|
transition.updateAlpha(node: self.receiveButtonNode, alpha: buttonAlpha)
|
||||||
@ -276,12 +384,19 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateIn() {
|
func becameReady(animated: Bool) {
|
||||||
|
if animated {
|
||||||
self.sendButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
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.receiveButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
self.balanceNode.isLoading = false
|
self.balanceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
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)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 address: String
|
||||||
private let openTransaction: (WalletTransaction) -> Void
|
private let openTransaction: (WalletTransaction) -> Void
|
||||||
|
|
||||||
|
private let hapticFeedback = HapticFeedback()
|
||||||
|
|
||||||
private let headerNode: WalletInfoHeaderNode
|
private let headerNode: WalletInfoHeaderNode
|
||||||
private let listNode: ListView
|
private let listNode: ListView
|
||||||
|
private let loadingIndicator: UIActivityIndicatorView
|
||||||
|
|
||||||
private var enqueuedTransactions: [WalletInfoListTransaction] = []
|
private var enqueuedTransactions: [WalletInfoListTransaction] = []
|
||||||
|
|
||||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||||
|
|
||||||
private let balanceDisposable = MetaDisposable()
|
private let stateDisposable = MetaDisposable()
|
||||||
private let transactionListDisposable = MetaDisposable()
|
private let transactionListDisposable = MetaDisposable()
|
||||||
|
|
||||||
private var listOffset: CGFloat?
|
private var listOffset: CGFloat?
|
||||||
|
private var reloadingState: Bool = false
|
||||||
private var loadingMoreTransactions: Bool = false
|
private var loadingMoreTransactions: Bool = false
|
||||||
private var canLoadMoreTransactions: Bool = true
|
private var canLoadMoreTransactions: Bool = true
|
||||||
|
|
||||||
|
private var combinedState: CombinedWalletState?
|
||||||
private var currentEntries: [WalletInfoListEntry]?
|
private var currentEntries: [WalletInfoListEntry]?
|
||||||
|
|
||||||
private var isReady: Bool = false
|
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) {
|
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.account = account
|
||||||
self.tonContext = tonContext
|
self.tonContext = tonContext
|
||||||
@ -384,47 +509,50 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
|||||||
self.address = address
|
self.address = address
|
||||||
self.openTransaction = openTransaction
|
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 = ListView()
|
||||||
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
|
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
|
||||||
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
|
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
|
||||||
self.listNode.isHidden = true
|
self.listNode.isHidden = true
|
||||||
|
|
||||||
|
self.loadingIndicator = UIActivityIndicatorView(style: .whiteLarge)
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.backgroundColor = .white
|
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.listNode)
|
||||||
self.addSubnode(self.headerNode)
|
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
|
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in
|
||||||
guard let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout else {
|
guard let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let headerHeight: CGFloat = navigationHeight + 260.0
|
||||||
strongSelf.listOffset = offset
|
strongSelf.listOffset = offset
|
||||||
|
|
||||||
if strongSelf.isReady {
|
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)
|
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 {
|
guard case let .known(value) = offset, value < 100.0 else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !strongSelf.loadingMoreTransactions && strongSelf.canLoadMoreTransactions {
|
if !strongSelf.loadingMoreTransactions && !strongSelf.reloadingState && strongSelf.canLoadMoreTransactions {
|
||||||
strongSelf.loadMoreTransactions()
|
strongSelf.loadMoreTransactions()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.listNode.didEndScrolling = { [weak self] in
|
self.listNode.didEndScrolling = { [weak self] in
|
||||||
|
canBeginRefresh = true
|
||||||
|
|
||||||
guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else {
|
guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -460,6 +590,26 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.refreshTransactions()
|
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() {
|
func scrollToHideHeader() {
|
||||||
@ -477,6 +627,9 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
|||||||
let isFirstLayout = self.validLayout == nil
|
let isFirstLayout = self.validLayout == nil
|
||||||
self.validLayout = (layout, navigationHeight)
|
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 headerHeight: CGFloat = navigationHeight + 260.0
|
||||||
let topInset: CGFloat = headerHeight
|
let topInset: CGFloat = headerHeight
|
||||||
|
|
||||||
@ -531,13 +684,73 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
|||||||
private func refreshTransactions() {
|
private func refreshTransactions() {
|
||||||
self.transactionListDisposable.set(nil)
|
self.transactionListDisposable.set(nil)
|
||||||
self.loadingMoreTransactions = true
|
self.loadingMoreTransactions = true
|
||||||
|
self.reloadingState = true
|
||||||
|
|
||||||
self.transactionListDisposable.set((getWalletTransactions(address: self.address, previousId: nil, tonInstance: self.tonContext.instance)
|
self.headerNode.timestampString = "updating"
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] transactions in
|
|
||||||
|
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 {
|
guard let strongSelf = self else {
|
||||||
return
|
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
|
}, error: { [weak self] _ in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
@ -576,13 +789,15 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
|||||||
self.loadingMoreTransactions = false
|
self.loadingMoreTransactions = false
|
||||||
self.canLoadMoreTransactions = transactions.count > 2
|
self.canLoadMoreTransactions = transactions.count > 2
|
||||||
|
|
||||||
let isFirst = self.currentEntries == nil
|
|
||||||
|
|
||||||
var updatedEntries: [WalletInfoListEntry] = []
|
var updatedEntries: [WalletInfoListEntry] = []
|
||||||
if isReload {
|
if isReload {
|
||||||
|
var existingIds = Set<WalletTransactionId>()
|
||||||
for transaction in transactions {
|
for transaction in transactions {
|
||||||
|
if !existingIds.contains(transaction.transactionId) {
|
||||||
|
existingIds.insert(transaction.transactionId)
|
||||||
updatedEntries.append(.transaction(updatedEntries.count, transaction))
|
updatedEntries.append(.transaction(updatedEntries.count, transaction))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if updatedEntries.isEmpty {
|
if updatedEntries.isEmpty {
|
||||||
updatedEntries.append(.empty(self.address))
|
updatedEntries.append(.empty(self.address))
|
||||||
}
|
}
|
||||||
@ -625,13 +840,6 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
|
|||||||
|
|
||||||
self.enqueuedTransactions.append(transaction)
|
self.enqueuedTransactions.append(transaction)
|
||||||
self.dequeueTransaction()
|
self.dequeueTransaction()
|
||||||
|
|
||||||
let wasReady = self.isReady
|
|
||||||
self.isReady = self.headerNode.balance != nil && self.currentEntries != nil
|
|
||||||
|
|
||||||
if self.isReady && !wasReady {
|
|
||||||
self.animateReadyIn()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func dequeueTransaction() {
|
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.listNode.isHidden = false
|
||||||
self.headerNode.animateIn()
|
self.loadingIndicator.stopAnimating()
|
||||||
|
self.loadingIndicator.isHidden = true
|
||||||
|
self.headerNode.becameReady(animated: animated)
|
||||||
if let (layout, navigationHeight) = self.validLayout {
|
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 titleFont = Font.medium(17.0)
|
||||||
private let textFont = Font.monospace(15.0)
|
private let textFont = Font.monospace(15.0)
|
||||||
|
private let descriptionFont = Font.regular(15.0)
|
||||||
private let dateFont = Font.regular(14.0)
|
private let dateFont = Font.regular(14.0)
|
||||||
private let directionFont = Font.regular(15.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 {
|
class WalletInfoTransactionItemNode: ListViewItemNode {
|
||||||
private let backgroundNode: ASDisplayNode
|
private let backgroundNode: ASDisplayNode
|
||||||
private let topStripeNode: ASDisplayNode
|
private let topStripeNode: ASDisplayNode
|
||||||
@ -93,6 +100,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
|||||||
private let directionNode: TextNode
|
private let directionNode: TextNode
|
||||||
private let iconNode: ASImageNode
|
private let iconNode: ASImageNode
|
||||||
private let textNode: TextNode
|
private let textNode: TextNode
|
||||||
|
private let descriptionNode: TextNode
|
||||||
private let dateNode: TextNode
|
private let dateNode: TextNode
|
||||||
|
|
||||||
private let activateArea: AccessibilityAreaNode
|
private let activateArea: AccessibilityAreaNode
|
||||||
@ -127,6 +135,11 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
|||||||
self.textNode.contentMode = .left
|
self.textNode.contentMode = .left
|
||||||
self.textNode.contentsScale = UIScreen.main.scale
|
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 = TextNode()
|
||||||
self.dateNode.isUserInteractionEnabled = false
|
self.dateNode.isUserInteractionEnabled = false
|
||||||
self.dateNode.contentMode = .left
|
self.dateNode.contentMode = .left
|
||||||
@ -143,6 +156,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
|||||||
self.addSubnode(self.iconNode)
|
self.addSubnode(self.iconNode)
|
||||||
self.addSubnode(self.directionNode)
|
self.addSubnode(self.directionNode)
|
||||||
self.addSubnode(self.textNode)
|
self.addSubnode(self.textNode)
|
||||||
|
self.addSubnode(self.descriptionNode)
|
||||||
self.addSubnode(self.dateNode)
|
self.addSubnode(self.dateNode)
|
||||||
|
|
||||||
self.addSubnode(self.activateArea)
|
self.addSubnode(self.activateArea)
|
||||||
@ -152,6 +166,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
|||||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||||
let makeDirectionLayout = TextNode.asyncLayout(self.directionNode)
|
let makeDirectionLayout = TextNode.asyncLayout(self.directionNode)
|
||||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||||
|
let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode)
|
||||||
let makeDateLayout = TextNode.asyncLayout(self.dateNode)
|
let makeDateLayout = TextNode.asyncLayout(self.dateNode)
|
||||||
|
|
||||||
let currentItem = self.item
|
let currentItem = self.item
|
||||||
@ -172,6 +187,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
|||||||
let titleColor: UIColor
|
let titleColor: UIColor
|
||||||
let transferredValue = item.walletTransaction.transferredValue
|
let transferredValue = item.walletTransaction.transferredValue
|
||||||
var text: String = ""
|
var text: String = ""
|
||||||
|
var description: String = ""
|
||||||
if transferredValue <= 0 {
|
if transferredValue <= 0 {
|
||||||
title = "\(formatBalanceText(transferredValue))"
|
title = "\(formatBalanceText(transferredValue))"
|
||||||
titleColor = item.theme.list.itemPrimaryTextColor
|
titleColor = item.theme.list.itemPrimaryTextColor
|
||||||
@ -184,7 +200,12 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
|||||||
if !text.isEmpty {
|
if !text.isEmpty {
|
||||||
text.append("\n")
|
text.append("\n")
|
||||||
}
|
}
|
||||||
text.append(message.destination)
|
text.append(formatAddress(message.destination))
|
||||||
|
|
||||||
|
if !description.isEmpty {
|
||||||
|
description.append("\n")
|
||||||
|
}
|
||||||
|
description.append(message.textMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -192,7 +213,8 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
|||||||
titleColor = item.theme.chatList.secretTitleColor
|
titleColor = item.theme.chatList.secretTitleColor
|
||||||
directionText = "from"
|
directionText = "from"
|
||||||
if let inMessage = item.walletTransaction.inMessage {
|
if let inMessage = item.walletTransaction.inMessage {
|
||||||
text = inMessage.source
|
text = formatAddress(inMessage.source)
|
||||||
|
description = inMessage.textMessage
|
||||||
} else {
|
} else {
|
||||||
text = "<unknown>"
|
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 (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
|
var insets: UIEdgeInsets
|
||||||
let separatorHeight = UIScreenPixel
|
let separatorHeight = UIScreenPixel
|
||||||
|
|
||||||
@ -221,8 +245,12 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
|||||||
let topInset: CGFloat = 11.0
|
let topInset: CGFloat = 11.0
|
||||||
let bottomInset: CGFloat = 11.0
|
let bottomInset: CGFloat = 11.0
|
||||||
let titleSpacing: CGFloat = 2.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)
|
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)
|
insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
|
||||||
var topHighlightInset: CGFloat = 0.0
|
var topHighlightInset: CGFloat = 0.0
|
||||||
if dateHeaderAtBottom, let header = item.header {
|
if dateHeaderAtBottom, let header = item.header {
|
||||||
@ -249,6 +277,7 @@ class WalletInfoTransactionItemNode: ListViewItemNode {
|
|||||||
|
|
||||||
let _ = titleApply()
|
let _ = titleApply()
|
||||||
let _ = textApply()
|
let _ = textApply()
|
||||||
|
let _ = descriptionApply()
|
||||||
let _ = dateApply()
|
let _ = dateApply()
|
||||||
let _ = directionApply()
|
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)
|
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.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.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))
|
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 SwiftSignalKit
|
||||||
import OverlayStatusController
|
import OverlayStatusController
|
||||||
import ItemListUI
|
import ItemListUI
|
||||||
|
import TextFormat
|
||||||
|
|
||||||
|
public enum WalletSecureStorageResetReason {
|
||||||
|
case notAvailable
|
||||||
|
case changed
|
||||||
|
}
|
||||||
|
|
||||||
public enum WalletSplashMode {
|
public enum WalletSplashMode {
|
||||||
case intro
|
case intro
|
||||||
@ -20,6 +26,8 @@ public enum WalletSplashMode {
|
|||||||
case restoreFailed
|
case restoreFailed
|
||||||
case sending(WalletInfo, String, Int64, String)
|
case sending(WalletInfo, String, Int64, String)
|
||||||
case sent(WalletInfo, Int64)
|
case sent(WalletInfo, Int64)
|
||||||
|
case secureStorageNotAvailable
|
||||||
|
case secureStorageReset(WalletSecureStorageResetReason)
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class WalletSplashScreen: ViewController {
|
public final class WalletSplashScreen: ViewController {
|
||||||
@ -75,7 +83,7 @@ public final class WalletSplashScreen: ViewController {
|
|||||||
})
|
})
|
||||||
case .sent:
|
case .sent:
|
||||||
self.navigationItem.setLeftBarButton(UIBarButtonItem(customDisplayNode: ASDisplayNode())!, animated: false)
|
self.navigationItem.setLeftBarButton(UIBarButtonItem(customDisplayNode: ASDisplayNode())!, animated: false)
|
||||||
case .created, .success, .restoreFailed:
|
case .created, .success, .restoreFailed, .secureStorageNotAvailable, .secureStorageReset:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +118,15 @@ public final class WalletSplashScreen: ViewController {
|
|||||||
}
|
}
|
||||||
controller.dismiss()
|
controller.dismiss()
|
||||||
(strongSelf.navigationController as? NavigationController)?.replaceController(strongSelf, with: WalletSplashScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .created(walletInfo, wordList)), animated: true)
|
(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):
|
case let .created(walletInfo, wordList):
|
||||||
strongSelf.push(WalletWordDisplayScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, walletInfo: walletInfo, wordList: 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:
|
case .sending:
|
||||||
break
|
break
|
||||||
|
case .secureStorageNotAvailable:
|
||||||
|
strongSelf.dismiss()
|
||||||
|
case .secureStorageReset:
|
||||||
|
strongSelf.push(WalletWordCheckScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, mode: .import))
|
||||||
}
|
}
|
||||||
}, secondaryAction: { [weak self] in
|
}, secondaryAction: { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
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))
|
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()
|
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.presentationData = presentationData
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.secondaryAction = secondaryAction
|
self.secondaryAction = secondaryAction
|
||||||
@ -212,7 +260,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
|||||||
let title: String
|
let title: String
|
||||||
let text: String
|
let text: String
|
||||||
let buttonText: String
|
let buttonText: String
|
||||||
let termsText: String
|
let termsText: NSAttributedString
|
||||||
let secondaryActionText: String
|
let secondaryActionText: String
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
@ -220,21 +268,23 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
|||||||
title = "Gram Wallet"
|
title = "Gram Wallet"
|
||||||
text = "Gram wallet allows you to make fast and secure blockchain-based payments without intermediaries."
|
text = "Gram wallet allows you to make fast and secure blockchain-based payments without intermediaries."
|
||||||
buttonText = "Create My Wallet"
|
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")
|
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/IntroIcon")
|
||||||
secondaryActionText = ""
|
secondaryActionText = ""
|
||||||
case .created:
|
case .created:
|
||||||
title = "Congratulations"
|
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."
|
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"
|
buttonText = "Proceed"
|
||||||
termsText = ""
|
termsText = NSAttributedString(string: "")
|
||||||
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/CreatedIcon")
|
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/CreatedIcon")
|
||||||
secondaryActionText = ""
|
secondaryActionText = ""
|
||||||
case .success:
|
case .success:
|
||||||
title = "Ready to go!"
|
title = "Ready to go!"
|
||||||
text = "You’re all set. Now you have a wallet that only you control - directly, without middlemen or bankers. "
|
text = "You’re all set. Now you have a wallet that only you control - directly, without middlemen or bankers. "
|
||||||
buttonText = "View My Wallet"
|
buttonText = "View My Wallet"
|
||||||
termsText = ""
|
termsText = NSAttributedString(string: "")
|
||||||
self.iconNode.image = nil
|
self.iconNode.image = nil
|
||||||
if let path = getAppBundle().path(forResource: "celebrate", ofType: "tgs") {
|
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.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
|
||||||
@ -245,7 +295,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
|||||||
title = "Too Bad"
|
title = "Too Bad"
|
||||||
text = "Without the secret words, you can't'nrestore access to the wallet."
|
text = "Without the secret words, you can't'nrestore access to the wallet."
|
||||||
buttonText = "Create a New Wallet"
|
buttonText = "Create a New Wallet"
|
||||||
termsText = ""
|
termsText = NSAttributedString(string: "")
|
||||||
self.iconNode.image = nil
|
self.iconNode.image = nil
|
||||||
if let path = getAppBundle().path(forResource: "sad", ofType: "tgs") {
|
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.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
|
||||||
@ -256,20 +306,47 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
|||||||
title = "Sending Grams"
|
title = "Sending Grams"
|
||||||
text = "Please wait a few seconds for your transaction to be processed..."
|
text = "Please wait a few seconds for your transaction to be processed..."
|
||||||
buttonText = ""
|
buttonText = ""
|
||||||
termsText = ""
|
termsText = NSAttributedString(string: "")
|
||||||
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/SendingIcon")
|
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/SendingIcon")
|
||||||
secondaryActionText = ""
|
secondaryActionText = ""
|
||||||
case let .sent(_, amount):
|
case let .sent(_, amount):
|
||||||
title = "Done!"
|
title = "Done!"
|
||||||
text = "\(amount) Grams have been sent."
|
text = "\(amount) Grams have been sent."
|
||||||
buttonText = "View My Wallet"
|
buttonText = "View My Wallet"
|
||||||
termsText = ""
|
termsText = NSAttributedString(string: "")
|
||||||
self.iconNode.image = nil
|
self.iconNode.image = nil
|
||||||
if let path = getAppBundle().path(forResource: "celebrate", ofType: "tgs") {
|
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.setup(account: account, resource: .localFile(path), width: 280, height: 280, mode: .direct)
|
||||||
self.animationNode.visibility = true
|
self.animationNode.visibility = true
|
||||||
}
|
}
|
||||||
secondaryActionText = ""
|
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()
|
self.titleNode = ImmediateTextNode()
|
||||||
@ -287,7 +364,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
|||||||
|
|
||||||
self.termsNode = ImmediateTextNode()
|
self.termsNode = ImmediateTextNode()
|
||||||
self.termsNode.displaysAsynchronously = false
|
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.maximumNumberOfLines = 0
|
||||||
self.termsNode.textAlignment = .center
|
self.termsNode.textAlignment = .center
|
||||||
|
|
||||||
@ -331,6 +408,20 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.secondaryActionButtonNode.addTarget(self, action: #selector(self.secondaryActionPressed), forControlEvents: .touchUpInside)
|
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() {
|
@objc private func secondaryActionPressed() {
|
||||||
|
|||||||
@ -27,6 +27,7 @@ private final class WalletTransactionInfoControllerArguments {
|
|||||||
private enum WalletTransactionInfoSection: Int32 {
|
private enum WalletTransactionInfoSection: Int32 {
|
||||||
case amount
|
case amount
|
||||||
case info
|
case info
|
||||||
|
case comment
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum WalletTransactionInfoEntry: ItemListNodeEntry {
|
private enum WalletTransactionInfoEntry: ItemListNodeEntry {
|
||||||
@ -35,6 +36,8 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
|
|||||||
case infoAddress(PresentationTheme, String)
|
case infoAddress(PresentationTheme, String)
|
||||||
case infoCopyAddress(PresentationTheme, String)
|
case infoCopyAddress(PresentationTheme, String)
|
||||||
case infoSendGrams(PresentationTheme, String)
|
case infoSendGrams(PresentationTheme, String)
|
||||||
|
case commentHeader(PresentationTheme, String)
|
||||||
|
case comment(PresentationTheme, String)
|
||||||
|
|
||||||
var section: ItemListSectionId {
|
var section: ItemListSectionId {
|
||||||
switch self {
|
switch self {
|
||||||
@ -42,6 +45,8 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
|
|||||||
return WalletTransactionInfoSection.amount.rawValue
|
return WalletTransactionInfoSection.amount.rawValue
|
||||||
case .infoHeader, .infoAddress, .infoCopyAddress, .infoSendGrams:
|
case .infoHeader, .infoAddress, .infoCopyAddress, .infoSendGrams:
|
||||||
return WalletTransactionInfoSection.info.rawValue
|
return WalletTransactionInfoSection.info.rawValue
|
||||||
|
case .commentHeader, .comment:
|
||||||
|
return WalletTransactionInfoSection.comment.rawValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +62,10 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
|
|||||||
return 3
|
return 3
|
||||||
case .infoSendGrams:
|
case .infoSendGrams:
|
||||||
return 4
|
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: {
|
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||||
arguments.sendGrams()
|
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
|
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] {
|
private func walletTransactionInfoControllerEntries(presentationData: PresentationData, walletTransaction: WalletTransaction, state: WalletTransactionInfoControllerState) -> [WalletTransactionInfoEntry] {
|
||||||
var entries: [WalletTransactionInfoEntry] = []
|
var entries: [WalletTransactionInfoEntry] = []
|
||||||
|
|
||||||
@ -118,16 +155,22 @@ private func walletTransactionInfoControllerEntries(presentationData: Presentati
|
|||||||
|
|
||||||
let transferredValue = walletTransaction.transferredValue
|
let transferredValue = walletTransaction.transferredValue
|
||||||
let text = extractAddress(walletTransaction)
|
let text = extractAddress(walletTransaction)
|
||||||
|
let description = extractDescription(walletTransaction)
|
||||||
|
|
||||||
if transferredValue <= 0 {
|
if transferredValue <= 0 {
|
||||||
entries.append(.infoHeader(presentationData.theme, "RECIPIENT"))
|
entries.append(.infoHeader(presentationData.theme, "RECIPIENT"))
|
||||||
} else {
|
} else {
|
||||||
entries.append(.infoHeader(presentationData.theme, "SENDER"))
|
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(.infoCopyAddress(presentationData.theme, "Copy Address"))
|
||||||
entries.append(.infoSendGrams(presentationData.theme, "Send Grams"))
|
entries.append(.infoSendGrams(presentationData.theme, "Send Grams"))
|
||||||
|
|
||||||
|
if !description.isEmpty {
|
||||||
|
entries.append(.commentHeader(presentationData.theme, "COMMENT"))
|
||||||
|
entries.append(.comment(presentationData.theme, description))
|
||||||
|
}
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,6 +338,8 @@ private class WalletTransactionHeaderItemNode: ListViewItemNode {
|
|||||||
contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset)
|
contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset)
|
||||||
let insets = itemListNeighborsGroupedInsets(neighbors)
|
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)
|
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||||
|
|
||||||
return (layout, { [weak self] in
|
return (layout, { [weak self] in
|
||||||
@ -311,9 +356,11 @@ private class WalletTransactionHeaderItemNode: ListViewItemNode {
|
|||||||
let contentWidth = titleLayout.size.width + iconSpacing + iconSize.width / 2.0
|
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 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)
|
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.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)
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import SolidRoundedButtonNode
|
|||||||
import UndoUI
|
import UndoUI
|
||||||
import AlertUI
|
import AlertUI
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
import TextFormat
|
||||||
|
|
||||||
private let possibleWordList: [String] = [
|
private let possibleWordList: [String] = [
|
||||||
"abandon",
|
"abandon",
|
||||||
@ -2063,10 +2064,8 @@ private let possibleWordList: [String] = [
|
|||||||
"zoo"
|
"zoo"
|
||||||
]
|
]
|
||||||
|
|
||||||
private let verifyWordIndices: [Int] = [4, 16, 21]
|
|
||||||
|
|
||||||
public enum WalletWordCheckMode {
|
public enum WalletWordCheckMode {
|
||||||
case verify(WalletInfo, [String])
|
case verify(WalletInfo, [String], [Int])
|
||||||
case `import`
|
case `import`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2113,11 +2112,11 @@ public final class WalletWordCheckScreen: ViewController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch strongSelf.mode {
|
switch strongSelf.mode {
|
||||||
case let .verify(walletInfo, wordList):
|
case let .verify(walletInfo, wordList, indices):
|
||||||
let enteredWords = (strongSelf.displayNode as! WalletWordCheckScreenNode).enteredWords
|
let enteredWords = (strongSelf.displayNode as! WalletWordCheckScreenNode).enteredWords
|
||||||
var isCorrect = true
|
var isCorrect = true
|
||||||
for i in 0 ..< enteredWords.count {
|
for i in 0 ..< enteredWords.count {
|
||||||
if enteredWords[i] != wordList[verifyWordIndices[i]] {
|
if enteredWords[i] != wordList[indices[i]] {
|
||||||
isCorrect = false
|
isCorrect = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -2580,23 +2579,27 @@ private final class WalletWordCheckScreenNode: ViewControllerTracingNode, UIScro
|
|||||||
self.iconNode.displaysAsynchronously = false
|
self.iconNode.displaysAsynchronously = false
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let text: String
|
let text: NSAttributedString
|
||||||
let buttonText: String
|
let buttonText: String
|
||||||
let secondaryActionText: String
|
let secondaryActionText: String
|
||||||
|
|
||||||
let wordIndices: [Int]
|
let wordIndices: [Int]
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
case .verify:
|
case let .verify(_, _, indices):
|
||||||
wordIndices = verifyWordIndices
|
wordIndices = indices
|
||||||
title = "Test Time!"
|
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"
|
buttonText = "Continue"
|
||||||
secondaryActionText = ""
|
secondaryActionText = ""
|
||||||
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/WordsCheckIcon")
|
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/WordsCheckIcon")
|
||||||
case .import:
|
case .import:
|
||||||
title = "24 Secret Words"
|
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"
|
buttonText = "Continue"
|
||||||
secondaryActionText = "I don't have those"
|
secondaryActionText = "I don't have those"
|
||||||
wordIndices = Array(0 ..< 24)
|
wordIndices = Array(0 ..< 24)
|
||||||
@ -2617,7 +2620,7 @@ private final class WalletWordCheckScreenNode: ViewControllerTracingNode, UIScro
|
|||||||
|
|
||||||
self.textNode = ImmediateTextNode()
|
self.textNode = ImmediateTextNode()
|
||||||
self.textNode.displaysAsynchronously = false
|
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.maximumNumberOfLines = 0
|
||||||
self.textNode.lineSpacing = 0.1
|
self.textNode.lineSpacing = 0.1
|
||||||
self.textNode.textAlignment = .center
|
self.textNode.textAlignment = .center
|
||||||
|
|||||||
@ -72,7 +72,15 @@ public final class WalletWordDisplayScreen: ViewController {
|
|||||||
}
|
}
|
||||||
})]), in: .window(.root))
|
})]), in: .window(.root))
|
||||||
} else {
|
} 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