diff --git a/NotificationContent/NotificationViewController.swift b/NotificationContent/NotificationViewController.swift index d53b74a556..7914eda7c9 100644 --- a/NotificationContent/NotificationViewController.swift +++ b/NotificationContent/NotificationViewController.swift @@ -67,7 +67,8 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi let apiId: Int32 = BuildConfig.shared().apiId let languagesCategory = "ios" - let appGroupName = "group.\(appBundleIdentifier[.. group.org.telegram.TelegramHD + keychain-access-groups + + $(AppIdentifierPrefix)org.telegram.TelegramHD + diff --git a/Share/Share-AppStoreLLC.entitlements b/Share/Share-AppStoreLLC.entitlements index c9a9054223..c27839f0dd 100644 --- a/Share/Share-AppStoreLLC.entitlements +++ b/Share/Share-AppStoreLLC.entitlements @@ -6,5 +6,9 @@ group.ph.telegra.Telegraph + keychain-access-groups + + $(AppIdentifierPrefix)ph.telegra.Telegraph + diff --git a/Share/Share-HockeyApp.entitlements b/Share/Share-HockeyApp.entitlements index 65f2a19d32..c7ccc18eca 100644 --- a/Share/Share-HockeyApp.entitlements +++ b/Share/Share-HockeyApp.entitlements @@ -6,5 +6,9 @@ group.org.telegram.Telegram-iOS + keychain-access-groups + + $(AppIdentifierPrefix)org.telegram.Telegram-iOS + diff --git a/Share/ShareRootController.swift b/Share/ShareRootController.swift index 4a2918b73f..2509adf8b4 100644 --- a/Share/ShareRootController.swift +++ b/Share/ShareRootController.swift @@ -107,7 +107,8 @@ class ShareRootController: UIViewController { let apiId: Int32 = BuildConfig.shared().apiId let languagesCategory = "ios" - let appGroupName = "group.\(appBundleIdentifier[.. mapToSignal { account -> Signal in if let account = account { switch account { diff --git a/Telegram-iOS.xcodeproj/project.pbxproj b/Telegram-iOS.xcodeproj/project.pbxproj index dcc2938c0b..b52e9be5ad 100644 --- a/Telegram-iOS.xcodeproj/project.pbxproj +++ b/Telegram-iOS.xcodeproj/project.pbxproj @@ -2769,6 +2769,9 @@ com.apple.BackgroundModes = { enabled = 1; }; + com.apple.Keychain = { + enabled = 1; + }; com.apple.Push = { enabled = 1; }; @@ -2804,6 +2807,9 @@ com.apple.ApplicationGroups.iOS = { enabled = 1; }; + com.apple.Keychain = { + enabled = 1; + }; }; }; D0B2F736204F4C9900D3BFB9 = { diff --git a/Telegram-iOS/AppDelegate.swift b/Telegram-iOS/AppDelegate.swift index 34321da6ed..0513ce032c 100644 --- a/Telegram-iOS/AppDelegate.swift +++ b/Telegram-iOS/AppDelegate.swift @@ -13,6 +13,8 @@ import CloudKit private let handleVoipNotifications = false +private var testIsLaunched = false + private func encodeText(_ string: String, _ key: Int) -> String { var result = "" for c in string.unicodeScalars { @@ -200,6 +202,9 @@ final class SharedApplicationContext { } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool { + precondition(!testIsLaunched) + testIsLaunched = true + let statusBarHost = ApplicationStatusBarHost() let (window, hostView) = nativeWindowHostView() self.mainWindow = Window1(hostView: hostView, statusBarHost: statusBarHost) @@ -346,7 +351,8 @@ final class SharedApplicationContext { let networkArguments = NetworkInitializationArguments(apiId: apiId, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: PresentationCallManager.voipMaxLayer, appData: BuildConfig.shared().bundleData) - let appGroupName = "group.\(Bundle.main.bundleIdentifier!)" + let baseAppBundleId = Bundle.main.bundleIdentifier! + let appGroupName = "group.\(baseAppBundleId)" let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) guard let appGroupUrl = maybeAppGroupUrl else { @@ -372,7 +378,8 @@ final class SharedApplicationContext { let rootPath = rootPathForBasePath(appGroupUrl.path) performAppGroupUpgrades(appGroupPath: appGroupUrl.path, rootPath: rootPath) - let encryptionKey = BuildConfig.encryptionKey(rootPath) + let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId) + let encryptionParameters = ValueBoxEncryptionParameters(key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!) TempBox.initializeShared(basePath: rootPath, processType: "app", launchSpecificId: arc4random64()) @@ -575,15 +582,15 @@ final class SharedApplicationContext { let accountManagerSignal = Signal { subscriber in let accountManager = AccountManager(basePath: rootPath + "/accounts-metadata") - return upgradedAccounts(accountManager: accountManager, rootPath: rootPath, encryptionKey: encryptionKey).start(completed: { + return upgradedAccounts(accountManager: accountManager, rootPath: rootPath, encryptionParameters: encryptionParameters).start(completed: { subscriber.putNext(accountManager) subscriber.putCompletion() }) - return EmptyDisposable } let sharedContextSignal = accountManagerSignal |> deliverOnMainQueue + |> take(1) |> mapToSignal { accountManager -> Signal<(SharedApplicationContext, LoggingSettings), NoError> in var initialPresentationDataAndSettings: InitialPresentationDataAndSettings? let semaphore = DispatchSemaphore(value: 0) @@ -601,7 +608,7 @@ final class SharedApplicationContext { let legacyCache = LegacyCache(path: legacyBasePath + "/Caches") var setPresentationCall: ((PresentationCall?) -> Void)? - let sharedContext = SharedAccountContext(mainWindow: self.mainWindow, basePath: rootPath, encryptionKey: encryptionKey, accountManager: accountManager, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: networkArguments, rootPath: rootPath, legacyBasePath: legacyBasePath, legacyCache: legacyCache, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), setNotificationCall: { call in + let sharedContext = SharedAccountContext(mainWindow: self.mainWindow, basePath: rootPath, encryptionParameters: encryptionParameters, accountManager: accountManager, applicationBindings: applicationBindings, initialPresentationDataAndSettings: initialPresentationDataAndSettings!, networkArguments: networkArguments, rootPath: rootPath, legacyBasePath: legacyBasePath, legacyCache: legacyCache, apsNotificationToken: self.notificationTokenPromise.get() |> map(Optional.init), voipNotificationToken: self.voipTokenPromise.get() |> map(Optional.init), setNotificationCall: { call in setPresentationCall?(call) }, navigateToChat: { accountId, peerId, messageId in self.openChatWhenReady(accountId: accountId, peerId: peerId, messageId: messageId) @@ -720,7 +727,7 @@ final class SharedApplicationContext { Logger.shared.logToConsole = loggingSettings.logToConsole Logger.shared.redactSensitiveData = loggingSettings.redactSensitiveData - return importedLegacyAccount(basePath: appGroupUrl.path, accountManager: sharedApplicationContext.sharedContext.accountManager, encryptionKey: encryptionKey, present: { controller in + return importedLegacyAccount(basePath: appGroupUrl.path, accountManager: sharedApplicationContext.sharedContext.accountManager, encryptionParameters: encryptionParameters, present: { controller in self.window?.rootViewController?.present(controller, animated: true, completion: nil) }) |> `catch` { _ -> Signal in diff --git a/Telegram-iOS/BuildConfig.h b/Telegram-iOS/BuildConfig.h index f0e898e28a..b84fc13cfd 100644 --- a/Telegram-iOS/BuildConfig.h +++ b/Telegram-iOS/BuildConfig.h @@ -1,5 +1,12 @@ #import +@interface DeviceSpecificEncryptionParameters : NSObject + +@property (nonatomic, strong) NSData * _Nonnull key; +@property (nonatomic, strong) NSData * _Nonnull salt; + +@end + @interface BuildConfig : NSObject + (instancetype _Nonnull)sharedBuildConfig; @@ -13,6 +20,6 @@ @property (nonatomic, readonly) int64_t appStoreId; @property (nonatomic, strong, readonly) NSString * _Nonnull appSpecificUrlScheme; -+ (NSData * _Nonnull)encryptionKey:(NSString * _Nonnull)rootPath; ++ (DeviceSpecificEncryptionParameters * _Nonnull)deviceSpecificEncryptionParameters:(NSString * _Nonnull)rootPath baseAppBundleId:(NSString * _Nonnull)baseAppBundleId; @end diff --git a/Telegram-iOS/BuildConfig.m b/Telegram-iOS/BuildConfig.m index 08b5d8102a..378dcad7b1 100644 --- a/Telegram-iOS/BuildConfig.m +++ b/Telegram-iOS/BuildConfig.m @@ -227,6 +227,62 @@ static MTPKCS * _Nullable checkSignature(const char *filename) { return result; } +@interface LocalPrivateKey : NSObject { + SecKeyRef _privateKey; + SecKeyRef _publicKey; +} + +- (NSData * _Nullable)encrypt:(NSData * _Nonnull)data; +- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data; + +@end + +@implementation LocalPrivateKey + +- (instancetype _Nonnull)initWithPrivateKey:(SecKeyRef)privateKey publicKey:(SecKeyRef)publicKey { + self = [super init]; + if (self != nil) { + _privateKey = (SecKeyRef)CFRetain(privateKey); + _publicKey = (SecKeyRef)CFRetain(publicKey); + } + return self; +} + +- (void)dealloc { + CFRelease(_privateKey); + CFRelease(_publicKey); +} + +- (NSData * _Nullable)encrypt:(NSData * _Nonnull)data { + if (data.length % 16 != 0) { + return nil; + } + + CFErrorRef error = NULL; + NSData *cipherText = (NSData *)CFBridgingRelease(SecKeyCreateEncryptedData(_publicKey, kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM, (__bridge CFDataRef)data, &error)); + + if (!cipherText) { + __unused NSError *err = CFBridgingRelease(error); + return nil; + } + + return cipherText; +} + +- (NSData * _Nullable)decrypt:(NSData * _Nonnull)data { + CFErrorRef error = NULL; + NSData *plainText = (NSData *)CFBridgingRelease(SecKeyCreateDecryptedData(_privateKey, kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM, (__bridge CFDataRef)data, &error)); + + if (!plainText) { + __unused NSError *err = CFBridgingRelease(error); + return nil; + } + + return plainText; +} + +@end + @interface BuildConfig () { NSData * _Nullable _bundleData; int32_t _apiId; @@ -236,6 +292,19 @@ static MTPKCS * _Nullable checkSignature(const char *filename) { @end +@implementation DeviceSpecificEncryptionParameters + +- (instancetype)initWithKey:(NSData * _Nonnull)key salt:(NSData * _Nonnull)salt { + self = [super init]; + if (self != nil) { + _key = key; + _salt = salt; + } + return self; +} + +@end + @implementation BuildConfig + (NSString *)bundleId { @@ -382,17 +451,205 @@ static MTPKCS * _Nullable checkSignature(const char *filename) { return @(APP_SPECIFIC_URL_SCHEME); } -+ (NSData * _Nonnull)encryptionKey:(NSString * _Nonnull)rootPath { - NSString *filePath = [rootPath stringByAppendingPathComponent:@".tempkey"]; - NSData *data = [NSData dataWithContentsOfFile:filePath]; - if (data != nil) { - return data; ++ (NSString * _Nullable)bundleSeedId { + NSDictionary *query = [NSDictionary dictionaryWithObjectsAndKeys: + (__bridge NSString *)kSecClassGenericPassword, (__bridge NSString *)kSecClass, + @"bundleSeedID", kSecAttrAccount, + @"", kSecAttrService, + (id)kCFBooleanTrue, kSecReturnAttributes, + nil]; + CFDictionaryRef result = nil; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); + if (status == errSecItemNotFound) { + status = SecItemAdd((__bridge CFDictionaryRef)query, (CFTypeRef *)&result); } - NSMutableData *randomData = [[NSMutableData alloc] initWithLength:32]; - int result = SecRandomCopyBytes(kSecRandomDefault, randomData.length, [randomData mutableBytes]); - assert(result == 0); - [randomData writeToFile:filePath atomically:false]; - return randomData; + if (status != errSecSuccess) { + return nil; + } + NSString *accessGroup = [(__bridge NSDictionary *)result objectForKey:(__bridge NSString *)kSecAttrAccessGroup]; + NSArray *components = [accessGroup componentsSeparatedByString:@"."]; + NSString *bundleSeedID = [[components objectEnumerator] nextObject]; + CFRelease(result); + return bundleSeedID; +} + ++ (LocalPrivateKey * _Nullable)getLocalPrivateKey:(NSString * _Nonnull)baseAppBundleId { + NSString *bundleSeedId = [self bundleSeedId]; + if (bundleSeedId == nil) { + return nil; + } + + NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId]; + + NSData *applicationTag = [@"telegramLocalKey" dataUsingEncoding:NSUTF8StringEncoding]; + + NSDictionary *query = @{ + (id)kSecClass: (id)kSecClassKey, + (id)kSecAttrApplicationTag: applicationTag, + (id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom, + (id)kSecAttrAccessGroup: (id)accessGroup, + (id)kSecReturnRef: @YES, + }; + 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)removeLocalPrivateKey:(NSString * _Nonnull)baseAppBundleId { + NSString *bundleSeedId = [self bundleSeedId]; + if (bundleSeedId == nil) { + return nil; + } + + NSData *applicationTag = [@"telegramLocalKey" 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)addLocalPrivateKey:(NSString * _Nonnull)baseAppBundleId { + NSString *bundleSeedId = [self bundleSeedId]; + if (bundleSeedId == nil) { + return nil; + } + + NSData *applicationTag = [@"telegramLocalKey" dataUsingEncoding:NSUTF8StringEncoding]; + NSString *accessGroup = [bundleSeedId stringByAppendingFormat:@".%@", baseAppBundleId]; + + SecAccessControlRef access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleAlwaysThisDeviceOnly, kSecAccessControlPrivateKeyUsage, NULL); + NSDictionary *attributes = @{ + (id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom, + (id)kSecAttrKeySizeInBits: @256, + (id)kSecAttrTokenID: (id)kSecAttrTokenIDSecureEnclave, + (id)kSecPrivateKeyAttrs: @{ + (id)kSecAttrIsPermanent: @YES, + (id)kSecAttrApplicationTag: applicationTag, + (id)kSecAttrAccessControl: (__bridge id)access, + (id)kSecAttrAccessGroup: (id)accessGroup, + }, + }; + + CFErrorRef error = NULL; + SecKeyRef privateKey = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &error); + if (!privateKey) { + if (access) { + CFRelease(access); + } + + __unused NSError *err = CFBridgingRelease(error); + return nil; + } + + 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; +} + ++ (DeviceSpecificEncryptionParameters * _Nonnull)deviceSpecificEncryptionParameters:(NSString * _Nonnull)rootPath baseAppBundleId:(NSString * _Nonnull)baseAppBundleId { + CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); + + NSString *filePath = [rootPath stringByAppendingPathComponent:@".tempkey"]; + NSString *encryptedPath = [rootPath stringByAppendingPathComponent:@".tempkeyEncrypted"]; + + NSData *currentData = [NSData dataWithContentsOfFile:filePath]; + NSData *resultData = nil; + if (currentData != nil && currentData.length == 32 + 16) { + resultData = currentData; + } + if (resultData == nil) { + NSMutableData *randomData = [[NSMutableData alloc] initWithLength:32 + 16]; + int result = SecRandomCopyBytes(kSecRandomDefault, randomData.length, [randomData mutableBytes]); + if (currentData != nil && currentData.length == 32) { // upgrade key with salt + [currentData getBytes:randomData.mutableBytes length:32]; + } + assert(result == 0); + resultData = randomData; + [resultData writeToFile:filePath atomically:false]; + } + + LocalPrivateKey *localPrivateKey = [self getLocalPrivateKey:baseAppBundleId]; + + if (localPrivateKey == nil) { + localPrivateKey = [self addLocalPrivateKey:baseAppBundleId]; + } + + NSData *currentEncryptedData = [NSData dataWithContentsOfFile:encryptedPath]; + + if (localPrivateKey != nil) { + if (currentEncryptedData != nil) { + NSData *decryptedData = [localPrivateKey decrypt:currentEncryptedData]; + + if (![resultData isEqualToData:decryptedData]) { + NSData *encryptedData = [localPrivateKey encrypt:resultData]; + [encryptedData writeToFile:encryptedPath atomically:false]; + assert(false); + } + } else { + NSData *encryptedData = [localPrivateKey encrypt:resultData]; + [encryptedData writeToFile:encryptedPath atomically:false]; + } + } + + CFAbsoluteTime endTime = CFAbsoluteTimeGetCurrent(); + NSLog(@"deviceSpecificEncryptionParameters took %f ms", (endTime - startTime) * 1000.0); + + NSData *key = [resultData subdataWithRange:NSMakeRange(0, 32)]; + NSData *salt = [resultData subdataWithRange:NSMakeRange(32, 16)]; + return [[DeviceSpecificEncryptionParameters alloc] initWithKey:key salt:salt]; } @end diff --git a/Telegram-iOS/LegacyDataImport.swift b/Telegram-iOS/LegacyDataImport.swift index 8086706845..612dfd8c95 100644 --- a/Telegram-iOS/LegacyDataImport.swift +++ b/Telegram-iOS/LegacyDataImport.swift @@ -106,7 +106,7 @@ enum ImportedLegacyAccountEvent { case result(AccountRecordId?) } -func importedLegacyAccount(basePath: String, accountManager: AccountManager, encryptionKey: Data, present: @escaping (UIViewController) -> Void) -> Signal { +func importedLegacyAccount(basePath: String, accountManager: AccountManager, encryptionParameters: ValueBoxEncryptionParameters, present: @escaping (UIViewController) -> Void) -> Signal { let queue = Queue() return deferred { () -> Signal in let documentsPath = basePath + "/Documents" @@ -218,7 +218,7 @@ func importedLegacyAccount(basePath: String, accountManager: AccountManager, enc } } - return temporaryAccount(manager: accountManager, rootPath: rootPathForBasePath(basePath), encryptionKey: encryptionKey) + return temporaryAccount(manager: accountManager, rootPath: rootPathForBasePath(basePath), encryptionParameters: encryptionParameters) |> introduceError(AccountImportError.self) |> mapToSignal { account -> Signal in let actions = importedAccountData(basePath: basePath, documentsPath: documentsPath, accountManager: accountManager, account: account, database: database) diff --git a/Telegram-iOS/Telegram-iOS-Hockeyapp.entitlements b/Telegram-iOS/Telegram-iOS-Hockeyapp.entitlements index 448f80b602..4b8f4137a4 100644 --- a/Telegram-iOS/Telegram-iOS-Hockeyapp.entitlements +++ b/Telegram-iOS/Telegram-iOS-Hockeyapp.entitlements @@ -28,5 +28,9 @@ group.org.telegram.Telegram-iOS + keychain-access-groups + + $(AppIdentifierPrefix)org.telegram.Telegram-iOS + diff --git a/Telegram-iOS/en.lproj/Localizable.strings b/Telegram-iOS/en.lproj/Localizable.strings index 12554b2f09..a98b555946 100644 --- a/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram-iOS/en.lproj/Localizable.strings @@ -1,69 +1,65 @@ // Notifications -"PUSH_MESSAGE_TEXT" = "%1$@: %2$@"; -"PUSH_MESSAGE_NOTEXT" = "%1$@ sent you a message"; -"PUSH_MESSAGE_PHOTO" = "%1$@ sent you a photo"; -"PUSH_MESSAGE_PHOTO_SECRET" = "%1$@ sent you a self-destructing photo"; -"PUSH_MESSAGE_VIDEO" = "%1$@ sent you a video"; -"PUSH_MESSAGE_VIDEO_SECRET" = "%1$@ sent you a self-destructing video"; -"PUSH_MESSAGE_ROUND" = "%1$@ sent you a video message"; -"PUSH_MESSAGE_CONTACT" = "%1$@ shared a contact with you"; -"PUSH_MESSAGE_GEO" = "%1$@ sent you a map"; -"PUSH_MESSAGE_GEOLIVE" = "%1$@ started sharing their live location"; -"PUSH_MESSAGE_DOC" = "%1$@ sent you a file"; -"PUSH_MESSAGE_AUDIO" = "%1$@ sent you a voice message"; -"PUSH_MESSAGE_GIF" = "%1$@ sent you a GIF"; +"PUSH_MESSAGE_TEXT" = "%1$@|%2$@"; +"PUSH_MESSAGE_NOTEXT" = "%1$@|sent you a message"; +"PUSH_MESSAGE_PHOTO" = "%1$@|sent you a photo"; +"PUSH_MESSAGE_PHOTO_SECRET" = "%1$@|sent you a self-destructing photo"; +"PUSH_MESSAGE_VIDEO" = "%1$@|sent you a video"; +"PUSH_MESSAGE_VIDEO_SECRET" = "%1$@|sent you a self-destructing video"; +"PUSH_MESSAGE_ROUND" = "%1$@|sent you a video message"; +"PUSH_MESSAGE_CONTACT" = "%1$@|shared a contact %2$@ with you"; +"PUSH_MESSAGE_GEO" = "%1$@|sent you a map"; +"PUSH_MESSAGE_GEOLIVE" = "%1$@|started sharing their live location"; +"PUSH_MESSAGE_DOC" = "%1$@|sent you a file"; +"PUSH_MESSAGE_AUDIO" = "%1$@|sent you a voice message"; +"PUSH_MESSAGE_GIF" = "%1$@|sent you a GIF"; "PUSH_ENCRYPTED_MESSAGE" = "You have a new message%1$@"; "PUSH_LOCKED_MESSAGE" = "You have a new message%1$@"; -"PUSH_MESSAGE_SCREENSHOT" = "%1$@ took a screenshot!"; +"PUSH_MESSAGE_SCREENSHOT" = "%1$@|took a screenshot!"; "PUSH_ENCRYPTION_REQUEST" = "New encryption request%1$@"; "PUSH_ENCRYPTION_ACCEPT" = "Your encryption request was accepted%1$@"; -"PUSH_MESSAGE_POLL" = "%1$@ sent you a poll"; -"PUSH_CHANNEL_MESSAGE_POLL" = "%1$@ posted a poll"; -"PUSH_CHAT_MESSAGE_POLL" = "%1$@ sent a poll to the group %2$@"; -"PUSH_PINNED_POLL" = "%1$@ pinned a poll"; +"PUSH_MESSAGE_POLL" = "%1$@|sent you a poll %2$@"; +"PUSH_CHANNEL_MESSAGE_POLL" = "%1$@|posted a poll %2$@"; +"PUSH_PINNED_POLL" = "%1$@|pinned a poll"; -"PUSH_CHAT_MESSAGE_TEXT" = "%1$@@%2$@: %3$@"; -"PUSH_CHAT_MESSAGE_NOTEXT" = "%1$@ sent a message to the group %2$@"; -"PUSH_CHAT_MESSAGE_PHOTO" = "%1$@ sent a photo to the group %2$@"; -"PUSH_CHAT_MESSAGE_VIDEO" = "%1$@ sent a video to the group %2$@"; -"PUSH_CHAT_MESSAGE_ROUND" = "%1$@ sent a video message to the group %2$@"; -"PUSH_CHAT_MESSAGE_CONTACT" = "%1$@ shared a contact in the group %2$@"; -"PUSH_CHAT_MESSAGE_GEO" = "%1$@ sent a map to the group %2$@"; -"PUSH_CHAT_MESSAGE_GEOLIVE" = "%1$@ started sharing their live location with %2$@"; -"PUSH_CHAT_MESSAGE_DOC" = "%1$@ sent a file to the group %2$@"; -"PUSH_CHAT_MESSAGE_AUDIO" = "%1$@ sent a voice message to the group %2$@"; -"PUSH_CHAT_MESSAGE_GIF" = "%1$@ sent a GIF to the group %2$@"; -"PUSH_CHAT_CREATED" = "%1$@ invited you to the group %2$@"; -"PUSH_CHAT_TITLE_EDITED" = "%1$@ edited the group's %2$@ name"; -"PUSH_CHAT_PHOTO_EDITED" = "%1$@ edited the group's %2$@ photo"; -"PUSH_CHAT_ADD_MEMBER" = "%1$@ invited %3$@ to the group %2$@"; -"PUSH_CHAT_ADD_YOU" = "%1$@ invited you to the group %2$@"; -"PUSH_CHAT_DELETE_YOU" = "%1$@ removed you from the group %2$@"; -"PUSH_CHAT_DELETE_MEMBER" = "%1$@ removed %3$@ from the group %2$@"; -"PUSH_CHAT_LEFT" = "%1$@ left the group %2$@"; -"PUSH_CHAT_RETURNED" = "%1$@ returned to the group %2$@"; -"PUSH_CHAT_MESSAGE_PHOTOS" = "%1$@ sent %3$@ photos to the group %2$@"; -"PUSH_CHAT_MESSAGES" = "%1$@ sent %3$@ messages to the group %2$@"; +"PUSH_CHAT_MESSAGE_TEXT" = "%2$@|%1$@: %3$@"; +"PUSH_CHAT_MESSAGE_NOTEXT" = "%2$@|%1$@ sent a message"; +"PUSH_CHAT_MESSAGE_PHOTO" = "%2$@|%1$@ sent a photo"; +"PUSH_CHAT_MESSAGE_VIDEO" = "%2$@|%1$@ sent a video"; +"PUSH_CHAT_MESSAGE_ROUND" = "%2$@|%1$@ sent a video message"; +"PUSH_CHAT_MESSAGE_CONTACT" = "%2$@|%1$@ shared a contact %3$@"; +"PUSH_CHAT_MESSAGE_GEO" = "%2$@|%1$@ sent a map"; +"PUSH_CHAT_MESSAGE_GEOLIVE" = "%2$@|%1$@ started sharing their live location"; +"PUSH_CHAT_MESSAGE_DOC" = "%2$@|%1$@ sent a file"; +"PUSH_CHAT_MESSAGE_AUDIO" = "%2$@|%1$@ sent a voice message"; +"PUSH_CHAT_MESSAGE_GIF" = "%2$@|%1$@ sent a GIF"; +"PUSH_CHAT_CREATED" = "%2$@|%1$@ invited you to the group"; +"PUSH_CHAT_TITLE_EDITED" = "%2$@|%1$@ edited the group's name"; +"PUSH_CHAT_PHOTO_EDITED" = "%2$@|%1$@ edited the group's photo"; +"PUSH_CHAT_ADD_MEMBER" = "%2$@|%1$@ invited %3$@ to the group"; +"PUSH_CHAT_ADD_YOU" = "%2$@|%1$@ invited you to the group"; +"PUSH_CHAT_DELETE_YOU" = "%2$@|%1$@ removed you from the group"; +"PUSH_CHAT_DELETE_MEMBER" = "%2$@|%1$@ removed %3$@ from the group"; +"PUSH_CHAT_LEFT" = "%2$@|%1$@ left the group"; +"PUSH_CHAT_RETURNED" = "%2$@|%1$@ returned to the group"; -"PUSH_MESSAGE_STICKER" = "%1$@ sent you a %2$@sticker"; -"PUSH_CHAT_MESSAGE_STICKER" = "%1$@ sent a %3$@sticker to the group %2$@"; +"PUSH_MESSAGE_STICKER" = "%1$@|sent you a %2$@sticker"; +"PUSH_CHAT_MESSAGE_STICKER" = "%2$@|%1$@ sent a %3$@sticker"; -"PUSH_CONTACT_JOINED" = "%1$@ joined Telegram!"; +"PUSH_CONTACT_JOINED" = "%1$@|joined Telegram!"; -"PUSH_CHANNEL_MESSAGE_TEXT" = "%1$@: %2$@"; -"PUSH_CHANNEL_MESSAGE_NOTEXT" = "%1$@ posted a message"; -"PUSH_CHANNEL_MESSAGE_PHOTO" = "%1$@ posted a photo"; -"PUSH_CHANNEL_MESSAGE_VIDEO" = "%1$@ posted a video"; -"PUSH_CHANNEL_MESSAGE_ROUND" = "%1$@ posted a video message"; -"PUSH_CHANNEL_MESSAGE_DOC" = "%1$@ posted a document"; -"PUSH_CHANNEL_MESSAGE_STICKER" = "%1$@ posted a %2$@sticker"; -"PUSH_CHANNEL_MESSAGE_AUDIO" = "%1$@ posted a voice message"; -"PUSH_CHANNEL_MESSAGE_CONTACT" = "%1$@ posted a contact"; -"PUSH_CHANNEL_MESSAGE_GEO" = "%1$@ posted a map"; -"PUSH_CHANNEL_MESSAGE_GEOLIVE" = "%1$@ posted a live location"; -"PUSH_CHANNEL_MESSAGE_GIF" = "%1$@ posted a GIF"; -"PUSH_CHANNEL_MESSAGES" = "%1$@ posted %2$@ messages"; +"PUSH_CHANNEL_MESSAGE_TEXT" = "%1$@|%2$@"; +"PUSH_CHANNEL_MESSAGE_NOTEXT" = "%1$@|posted a message"; +"PUSH_CHANNEL_MESSAGE_PHOTO" = "%1$@|posted a photo"; +"PUSH_CHANNEL_MESSAGE_VIDEO" = "%1$@|posted a video"; +"PUSH_CHANNEL_MESSAGE_ROUND" = "%1$@|posted a video message"; +"PUSH_CHANNEL_MESSAGE_DOC" = "%1$@|posted a document"; +"PUSH_CHANNEL_MESSAGE_STICKER" = "%1$@|posted a %2$@sticker"; +"PUSH_CHANNEL_MESSAGE_AUDIO" = "%1$@|posted a voice message"; +"PUSH_CHANNEL_MESSAGE_CONTACT" = "%1$@|posted a %2$@ contact"; +"PUSH_CHANNEL_MESSAGE_GEO" = "%1$@|posted a map"; +"PUSH_CHANNEL_MESSAGE_GEOLIVE" = "%1$@|posted a live location"; +"PUSH_CHANNEL_MESSAGE_GIF" = "%1$@|posted a GIF"; "PUSH_PINNED_TEXT" = "%1$@ pinned \"%2$@\" "; "PUSH_PINNED_NOTEXT" = "%1$@ pinned a message"; @@ -73,14 +69,13 @@ "PUSH_PINNED_DOC" = "%1$@ pinned a file"; "PUSH_PINNED_STICKER" = "%1$@ pinned a %2$@sticker"; "PUSH_PINNED_AUDIO" = "%1$@ pinned a voice message"; -"PUSH_PINNED_CONTACT" = "%1$@ pinned a contact"; "PUSH_PINNED_GEO" = "%1$@ pinned a map"; "PUSH_PINNED_GEOLIVE" = "%1$@ pinned a live location"; "PUSH_PINNED_GIF" = "%1$@ pinned a GIF"; -"PUSH_MESSAGE_GAME" = "%1$@ invited you to play %2$@"; -"PUSH_CHANNEL_MESSAGE_GAME" = "%1$@ invited you to play %2$@"; -"PUSH_CHAT_MESSAGE_GAME" = "%1$@ invited the group %2$@ to play %3$@"; +"PUSH_MESSAGE_GAME" = "%1$@|invited you to play %2$@"; +"PUSH_CHANNEL_MESSAGE_GAME" = "%1$@|invited you to play %2$@"; +"PUSH_CHAT_MESSAGE_GAME" = "%2$@|%1$@ invited the group to play %3$@"; "PUSH_PINNED_GAME" = "%1$@ pinned a game"; "PUSH_MESSAGE_TEXT" = "%1$@|%2$@"; @@ -116,6 +111,7 @@ "PUSH_MESSAGE" = "%1$@|sent you a message"; "PUSH_MESSAGES_1" = "%1$@|sent you a message"; "PUSH_MESSAGES_any" = "%1$@|sent you %2$d messages"; +"PUSH_MESSAGE_ALBUM" = "%1$@|sent you an album"; "PUSH_CHANNEL_MESSAGE_TEXT" = "%1$@|%2$@"; "PUSH_CHANNEL_MESSAGE_NOTEXT" = "%1$@|posted a message"; @@ -140,16 +136,16 @@ "PUSH_CHANNEL_MESSAGE_VIDEO" = "%1$@|posted a video"; "PUSH_CHANNEL_MESSAGE_VIDEOS_1" = "%1$@|posted a video"; "PUSH_CHANNEL_MESSAGE_VIDEOS_any" = "%1$@|posted %2$d videos"; -"PUSH_CHANNEL_MESSAGE_ROUND" = "%1$@|posted a round video"; -"PUSH_CHANNEL_MESSAGE_ROUNDS_1" = "%1$@|posted a round video"; -"PUSH_CHANNEL_MESSAGE_ROUNDS_any" = "%1$@|posted %2$d round videos"; +"PUSH_CHANNEL_MESSAGE_ROUND" = "%1$@|posted a video message"; +"PUSH_CHANNEL_MESSAGE_ROUNDS_1" = "%1$@|posted a video message"; +"PUSH_CHANNEL_MESSAGE_ROUNDS_any" = "%1$@|posted %2$d video messages"; "PUSH_CHANNEL_MESSAGE" = "%1$@|posted a message"; "PUSH_CHANNEL_MESSAGES_1" = "%1$@|posted a message"; "PUSH_CHANNEL_MESSAGES_any" = "%1$@|posted %2$d messages"; +"PUSH_CHANNEL_ALBUM" = "%1$@|posted an album"; "PUSH_CHAT_MESSAGE_TEXT" = "%2$@|%1$@:%3$@"; "PUSH_CHAT_MESSAGE_NOTEXT" = "%2$@|%1$@ sent a message to the group"; -"PUSH_CHAT_MESSAGE_PHOTO" = "%2$@|%1$@ sent a photo"; "PUSH_CHAT_MESSAGE_VIDEO" = "%2$@|%1$@ sent a video "; "PUSH_CHAT_MESSAGE_ROUND" = "%2$@|%1$@ sent a video message"; "PUSH_CHAT_MESSAGE_DOC" = "%2$@|%1$@ sent a file"; @@ -158,7 +154,7 @@ "PUSH_CHAT_MESSAGE_CONTACT" = "%2$@|%1$@ shared a contact"; "PUSH_CHAT_MESSAGE_GEO" = "%2$@|%1$@ sent a map"; "PUSH_CHAT_MESSAGE_GEOLIVE" = "%2$@|%1$@ started sharing their live location"; -"PUSH_CHAT_MESSAGE_POLL" = "%2$@|%1$@ sent a poll"; +"PUSH_CHAT_MESSAGE_POLL" = "%2$@|%1$@ sent a poll %3$@ to the group"; "PUSH_CHAT_MESSAGE_GIF" = "%2$@|%1$@ sent a GIF"; "PUSH_CHAT_MESSAGE_GAME" = "%2$@|%1$@ invited the group to play %3$@"; "PUSH_CHAT_MESSAGE_INVOICE" = "%2$@|%1$@ sent an invoice for %3$@"; @@ -187,6 +183,7 @@ "PUSH_CHAT_MESSAGE" = "%2$@|%1$@ sent a message"; "PUSH_CHAT_MESSAGES_1" = "%2$@|%1$@ sent a message"; "PUSH_CHAT_MESSAGES_any" = "%2$@|%1$@ sent %3$d messages"; +"PUSH_CHAT_ALBUM" = "%2$@|%1$@ sent an album"; "PUSH_PINNED_TEXT" = "%1$@|pinned \"%2$@\" "; "PUSH_PINNED_NOTEXT" = "%1$@|pinned a message"; @@ -196,10 +193,10 @@ "PUSH_PINNED_DOC" = "%1$@|pinned a file"; "PUSH_PINNED_STICKER" = "%1$@|pinned a %2$@sticker"; "PUSH_PINNED_AUDIO" = "%1$@|pinned a voice message"; -"PUSH_PINNED_CONTACT" = "%1$@|pinned a contact"; +"PUSH_PINNED_CONTACT" = "%1$@|pinned a %2$@ contact"; "PUSH_PINNED_GEO" = "%1$@|pinned a map"; "PUSH_PINNED_GEOLIVE" = "%1$@|pinned a live location"; -"PUSH_PINNED_POLL" = "|%1$@|pinned a poll"; +"PUSH_PINNED_POLL" = "|%1$@|pinned a poll %2$@"; "PUSH_PINNED_GAME" = "%1$@|pinned a game"; "PUSH_PINNED_INVOICE" = "%1$@|pinned an invoice"; "PUSH_PINNED_GIF" = "%1$@|pinned a GIF"; @@ -212,6 +209,14 @@ "PUSH_PHONE_CALL_REQUEST" = "%1$@|is calling you!"; "PUSH_PHONE_CALL_MISSED" = "%1$@|You missed a call"; +"PUSH_MESSAGE_GAME_SCORE" = "%1$@ scored %3$@ in game %2$@"; +"PUSH_MESSAGE_VIDEOS" = "%1$@ sent you %2$@ videos"; +"PUSH_MESSAGE_CHANNEL_MESSAGE_GAME_SCORE" = "%1$@ scored %3$@ in game %2$@"; +"PUSH_CHANNEL_MESSAGE_VIDEOS" = "%1$@ posted %2$@ videos"; +"PUSH_PINNED_GAME_SCORE" = "%1$@ pinned a game score"; +"PUSH_CHAT_MESSAGE_GAME_SCORE" = "%1$@ scored %4$@ in game %3$@ in the group %2$@"; +"PUSH_CHAT_MESSAGE_VIDEOS" = "%1$@ sent %3$@ videos to the group %2$@"; + "LOCAL_MESSAGE_FWDS" = "%1$@ forwarded you %2$d messages"; "LOCAL_CHANNEL_MESSAGE_FWDS" = "%1$@ posted %2$d forwarded messages"; "LOCAL_CHAT_MESSAGE_FWDS" = "%1$@ forwarded %2$d messages"; @@ -4211,3 +4216,5 @@ Unused sets are archived when you add more."; "Call.Flip" = "flip"; "Call.End" = "end"; "Call.Speaker" = "speaker"; + +"MemberSearch.BotSection" = "BOTS"; diff --git a/Widget/TodayViewController.swift b/Widget/TodayViewController.swift index c701a44b70..c27fea7475 100644 --- a/Widget/TodayViewController.swift +++ b/Widget/TodayViewController.swift @@ -57,7 +57,8 @@ class TodayViewController: UIViewController, NCWidgetProviding { let apiId: Int32 = BuildConfig.shared().apiId let languagesCategory = "ios" - let appGroupName = "group.\(appBundleIdentifier[.. mapToSignal { account -> Signal in if let account = account { switch account { diff --git a/submodules/Display b/submodules/Display index 7d0164259f..d80864dc37 160000 --- a/submodules/Display +++ b/submodules/Display @@ -1 +1 @@ -Subproject commit 7d0164259f2aa48516393a43822b5b8ab701d843 +Subproject commit d80864dc37cd4ac3aecae93d2d278051fbb8f921 diff --git a/submodules/MtProtoKit b/submodules/MtProtoKit index 579d7f6264..91e7d8de3c 160000 --- a/submodules/MtProtoKit +++ b/submodules/MtProtoKit @@ -1 +1 @@ -Subproject commit 579d7f626427d0cc82209c1b2fc3be7b97eac68c +Subproject commit 91e7d8de3cbf410eef555867edb948f6a107d44c diff --git a/submodules/Postbox b/submodules/Postbox index 5e6b155c5f..73d45d3c87 160000 --- a/submodules/Postbox +++ b/submodules/Postbox @@ -1 +1 @@ -Subproject commit 5e6b155c5fabf22f0229b700f32918e79dcbc6f4 +Subproject commit 73d45d3c87aa5837189521f41e4709a5d3780825 diff --git a/submodules/SSignalKit b/submodules/SSignalKit index 796552703a..2deb9c7541 160000 --- a/submodules/SSignalKit +++ b/submodules/SSignalKit @@ -1 +1 @@ -Subproject commit 796552703ada04403a3f4f8422c5c4696fd9c066 +Subproject commit 2deb9c75411eb395d1864da81978da2c3c95f07b diff --git a/submodules/TelegramCore b/submodules/TelegramCore index 414fc5cd9f..ab41021ba7 160000 --- a/submodules/TelegramCore +++ b/submodules/TelegramCore @@ -1 +1 @@ -Subproject commit 414fc5cd9f8ae41a95af4dc72f205052313e76fd +Subproject commit ab41021ba7e7d73e248b30cd5b4277720311fce7 diff --git a/submodules/TelegramUI b/submodules/TelegramUI index 11eaf77f43..8fbcfd0e88 160000 --- a/submodules/TelegramUI +++ b/submodules/TelegramUI @@ -1 +1 @@ -Subproject commit 11eaf77f43214a2dc716b17c91147e87ec8c941f +Subproject commit 8fbcfd0e888a5fbd9cc788f13998d67d63731134 diff --git a/tools/GenerateLocalization.swift b/tools/GenerateLocalization.swift index 9c86482f2e..97ce585064 100644 --- a/tools/GenerateLocalization.swift +++ b/tools/GenerateLocalization.swift @@ -389,6 +389,7 @@ public final class PresentationStrings { public let primaryComponent: PresentationStringsComponent public let secondaryComponent: PresentationStringsComponent? public let baseLanguageCode: String + public let groupingSeparator: String private let _s: [Int: String] private let _r: [Int: [(Int, NSRange)]] @@ -490,7 +491,8 @@ public final class PresentationStrings { """ public func \(escapedIdentifier(key))(_ value: Int32) -> String { let form = presentationStringsPluralizationForm(self.lc, value) - return String(format: self._ps[\(id) * \(PluralizationForm.formCount) + Int(form.rawValue)]!, \"\\(value)\") + let stringValue = presentationStringsFormattedNumber(value, self.groupingSeparator) + return String(format: self._ps[\(id) * \(PluralizationForm.formCount) + Int(form.rawValue)]!, stringValue) } """ @@ -503,9 +505,10 @@ public final class PresentationStrings { result += """ - init(primaryComponent: PresentationStringsComponent, secondaryComponent: PresentationStringsComponent?) { + init(primaryComponent: PresentationStringsComponent, secondaryComponent: PresentationStringsComponent?, groupingSeparator: String) { self.primaryComponent = primaryComponent self.secondaryComponent = secondaryComponent + self.groupingSeparator = groupingSeparator self.baseLanguageCode = secondaryComponent?.languageCode ?? primaryComponent.languageCode