Merge commit 'd1bcfbc670080b90e187aa42075670febd43685e'

This commit is contained in:
Peter 2019-08-07 23:58:52 +03:00
commit 558bf41c4a
12 changed files with 634 additions and 567 deletions

View File

@ -98,30 +98,30 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
let encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!) let encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!)
account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, appData: .single(buildConfig.bundleData(withAppToken: nil))), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters) account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, appData: .single(buildConfig.bundleData(withAppToken: nil))), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters)
|> mapToSignal { account -> Signal<Account?, NoError> in |> mapToSignal { account -> Signal<Account?, NoError> in
if let account = account { if let account = account {
switch account { switch account {
case .upgrading: case .upgrading:
return .complete() return .complete()
case let .authorized(account): case let .authorized(account):
return applicationSettings(accountManager: accountManager) return applicationSettings(accountManager: accountManager)
|> deliverOnMainQueue |> deliverOnMainQueue
|> map { settings -> Account in |> map { settings -> Account in
accountCache = account accountCache = account
Logger.shared.logToFile = settings.logging.logToFile Logger.shared.logToFile = settings.logging.logToFile
Logger.shared.logToConsole = settings.logging.logToConsole Logger.shared.logToConsole = settings.logging.logToConsole
Logger.shared.redactSensitiveData = settings.logging.redactSensitiveData Logger.shared.redactSensitiveData = settings.logging.redactSensitiveData
return account return account
} }
case .unauthorized: case .unauthorized:
return .complete() return .complete()
}
} else {
return .single(nil)
} }
} else {
return .single(nil)
} }
} |> take(1)
|> take(1)
} }
self.accountPromise.set(account) self.accountPromise.set(account)
} }
@ -135,14 +135,48 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
return self return self
} }
private func resolve(persons: [INPerson]?, with completion: @escaping ([INPersonResolutionResult]) -> Void) { enum ResolveResult {
guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else { case success(INPerson)
completion([INPersonResolutionResult.notRequired()]) case disambiguation([INPerson])
return case needsValue
case noResult
case skip
@available(iOSApplicationExtension 11.0, *)
var sendMessageRecipientResulutionResult: INSendMessageRecipientResolutionResult {
switch self {
case let .success(person):
return .success(with: person)
case let .disambiguation(persons):
return .disambiguation(with: persons)
case .needsValue:
return .needsValue()
case .noResult:
return .unsupported()
case .skip:
return .notRequired()
}
} }
var personResolutionResult: INPersonResolutionResult {
switch self {
case let .success(person):
return .success(with: person)
case let .disambiguation(persons):
return .disambiguation(with: persons)
case .needsValue:
return .needsValue()
case .noResult:
return .unsupported()
case .skip:
return .notRequired()
}
}
}
private func resolve(persons: [INPerson]?, with completion: @escaping ([ResolveResult]) -> Void) {
guard let initialPersons = persons, !initialPersons.isEmpty else { guard let initialPersons = persons, !initialPersons.isEmpty else {
completion([INPersonResolutionResult.needsValue()]) completion([.needsValue])
return return
} }
@ -164,12 +198,12 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
} }
if filteredPersons.isEmpty { if filteredPersons.isEmpty {
completion([INPersonResolutionResult.needsValue()]) completion([.noResult])
return return
} }
if filteredPersons.count > 1 { if filteredPersons.count > 1 {
completion([INPersonResolutionResult.disambiguation(with: filteredPersons)]) completion([.disambiguation(filteredPersons)])
return return
} }
@ -182,7 +216,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
} }
if allPersonsAlreadyMatched { if allPersonsAlreadyMatched {
completion([INPersonResolutionResult.success(with: filteredPersons[0])]) completion([.success(filteredPersons[0])])
return return
} }
@ -205,74 +239,84 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
let account = self.accountPromise.get() let account = self.accountPromise.get()
let signal = matchingDeviceContacts(stableIds: stableIds) let signal = matchingDeviceContacts(stableIds: stableIds)
|> take(1) |> take(1)
|> mapToSignal { matchedContacts in |> mapToSignal { matchedContacts in
return account return account
|> introduceError(IntentContactsError.self)
|> mapToSignal { account -> Signal<[(String, TelegramUser)], IntentContactsError> in
if let account = account {
return matchingCloudContacts(postbox: account.postbox, contacts: matchedContacts)
|> introduceError(IntentContactsError.self) |> introduceError(IntentContactsError.self)
} else { |> mapToSignal { account -> Signal<[(String, TelegramUser)], IntentContactsError> in
return .fail(.generic) if let account = account {
return matchingCloudContacts(postbox: account.postbox, contacts: matchedContacts)
|> introduceError(IntentContactsError.self)
} else {
return .fail(.generic)
}
} }
}
} }
self.resolvePersonsDisposable.set((signal self.resolvePersonsDisposable.set((signal
|> deliverOnMainQueue).start(next: { peers in |> deliverOnMainQueue).start(next: { peers in
if peers.isEmpty { if peers.isEmpty {
completion([INPersonResolutionResult.needsValue()]) completion([.needsValue])
} else { } else {
completion(peers.map { stableId, user in completion(peers.map { .success(personWithUser(stableId: $0, user: $1)) })
let person = personWithUser(stableId: stableId, user: user) }
return INPersonResolutionResult.success(with: person) }, error: { error in
}) completion([.skip])
} }))
}, error: { error in
completion([INPersonResolutionResult.unsupported()])
}))
} }
// MARK: - INSendMessageIntentHandling // MARK: - INSendMessageIntentHandling
func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INPersonResolutionResult]) -> Void) { func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INPersonResolutionResult]) -> Void) {
if #available(iOSApplicationExtension 11.0, *) { guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
if let peerId = intent.conversationIdentifier.flatMap(Int64.init) { completion([INPersonResolutionResult.notRequired()])
let account = self.accountPromise.get() return
}
let signal = account self.resolve(persons: intent.recipients, with: { result in
completion(result.map { $0.personResolutionResult })
})
}
@available(iOSApplicationExtension 11.0, *)
func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INSendMessageRecipientResolutionResult]) -> Void) {
if let peerId = intent.conversationIdentifier.flatMap(Int64.init) {
let account = self.accountPromise.get()
let signal = account
|> introduceError(IntentHandlingError.self) |> introduceError(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) |> introduceError(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)
} else { } else {
return nil return nil
} }
} }
} else { } else {
return .fail(.generic) return .fail(.generic)
} }
} }
self.resolvePersonsDisposable.set((signal self.resolvePersonsDisposable.set((signal
|> deliverOnMainQueue).start(next: { person in |> deliverOnMainQueue).start(next: { person in
if let person = person { if let person = person {
completion([INPersonResolutionResult.success(with: person)]) completion([INSendMessageRecipientResolutionResult.success(with: person)])
} else { } else {
completion([INPersonResolutionResult.needsValue()]) completion([INSendMessageRecipientResolutionResult.needsValue()])
} }
}, error: { error in }, error: { error in
completion([INPersonResolutionResult.notRequired()]) completion([INSendMessageRecipientResolutionResult.unsupported(forReason: .noAccount)])
})) }))
} else {
self.resolve(persons: intent.recipients, with: completion)
}
} else { } else {
self.resolve(persons: intent.recipients, with: completion) guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
completion([INSendMessageRecipientResolutionResult.notRequired()])
return
}
self.resolve(persons: intent.recipients, with: { result in
completion(result.map { $0.sendMessageRecipientResulutionResult })
})
} }
} }
@ -301,48 +345,48 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
func handle(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) { func handle(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) {
self.actionDisposable.set((self.accountPromise.get() self.actionDisposable.set((self.accountPromise.get()
|> take(1) |> take(1)
|> mapError { _ -> IntentHandlingError in
return .generic
}
|> mapToSignal { account -> Signal<Void, IntentHandlingError> in
guard let account = account else {
return .fail(.generic)
}
guard let recipient = intent.recipients?.first, let customIdentifier = recipient.customIdentifier, customIdentifier.hasPrefix("tg") else {
return .fail(.generic)
}
guard let peerIdValue = Int64(String(customIdentifier[customIdentifier.index(customIdentifier.startIndex, offsetBy: 2)...])) else {
return .fail(.generic)
}
let peerId = PeerId(peerIdValue)
if peerId.namespace != Namespaces.Peer.CloudUser {
return .fail(.generic)
}
account.shouldBeServiceTaskMaster.set(.single(.now))
return standaloneSendMessage(account: account, peerId: peerId, text: intent.content ?? "", attributes: [], media: nil, replyToMessageId: nil)
|> mapError { _ -> IntentHandlingError in |> mapError { _ -> IntentHandlingError in
return .generic return .generic
} }
|> mapToSignal { _ -> Signal<Void, IntentHandlingError> in |> mapToSignal { account -> Signal<Void, IntentHandlingError> in
return .complete() guard let account = account else {
return .fail(.generic)
}
guard let recipient = intent.recipients?.first, let customIdentifier = recipient.customIdentifier, customIdentifier.hasPrefix("tg") else {
return .fail(.generic)
}
guard let peerIdValue = Int64(String(customIdentifier[customIdentifier.index(customIdentifier.startIndex, offsetBy: 2)...])) else {
return .fail(.generic)
}
let peerId = PeerId(peerIdValue)
if peerId.namespace != Namespaces.Peer.CloudUser {
return .fail(.generic)
}
account.shouldBeServiceTaskMaster.set(.single(.now))
return standaloneSendMessage(account: account, peerId: peerId, text: intent.content ?? "", attributes: [], media: nil, replyToMessageId: nil)
|> mapError { _ -> IntentHandlingError in
return .generic
}
|> mapToSignal { _ -> Signal<Void, IntentHandlingError> in
return .complete()
}
|> afterDisposed {
account.shouldBeServiceTaskMaster.set(.single(.never))
}
} }
|> afterDisposed { |> deliverOnMainQueue).start(error: { _ in
account.shouldBeServiceTaskMaster.set(.single(.never)) let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
} let response = INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity)
} completion(response)
|> deliverOnMainQueue).start(error: { _ in }, completed: {
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self)) let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
let response = INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) let response = INSendMessageIntentResponse(code: .success, userActivity: userActivity)
completion(response) completion(response)
}, completed: { }))
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self))
let response = INSendMessageIntentResponse(code: .success, userActivity: userActivity)
completion(response)
}))
} }
// MARK: - INSearchForMessagesIntentHandling // MARK: - INSearchForMessagesIntentHandling
@ -353,42 +397,42 @@ 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)
|> introduceError(IntentHandlingError.self)
|> mapToSignal { account -> Signal<[INMessage], IntentHandlingError> in
guard let account = account else {
return .fail(.generic)
}
account.shouldBeServiceTaskMaster.set(.single(.now))
account.resetStateManagement()
let completion: Signal<Void, NoError> = account.stateManager.pollStateUpdateCompletion()
|> map { _ in
return Void()
}
return (completion |> timeout(4.0, queue: Queue.mainQueue(), alternate: .single(Void())))
|> introduceError(IntentHandlingError.self)
|> take(1) |> take(1)
|> mapToSignal { _ -> Signal<[INMessage], IntentHandlingError> in |> introduceError(IntentHandlingError.self)
return unreadMessages(account: account) |> mapToSignal { account -> Signal<[INMessage], IntentHandlingError> in
|> introduceError(IntentHandlingError.self) guard let account = account else {
|> afterDisposed { return .fail(.generic)
account.shouldBeServiceTaskMaster.set(.single(.never)) }
account.shouldBeServiceTaskMaster.set(.single(.now))
account.resetStateManagement()
let completion: Signal<Void, NoError> = account.stateManager.pollStateUpdateCompletion()
|> map { _ in
return Void()
}
return (completion |> timeout(4.0, queue: Queue.mainQueue(), alternate: .single(Void())))
|> introduceError(IntentHandlingError.self)
|> take(1)
|> mapToSignal { _ -> Signal<[INMessage], IntentHandlingError> in
return unreadMessages(account: account)
|> introduceError(IntentHandlingError.self)
|> afterDisposed {
account.shouldBeServiceTaskMaster.set(.single(.never))
}
} }
} }
} |> deliverOnMainQueue).start(next: { messages in
|> deliverOnMainQueue).start(next: { messages in let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self))
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self)) let response = INSearchForMessagesIntentResponse(code: .success, userActivity: userActivity)
let response = INSearchForMessagesIntentResponse(code: .success, userActivity: userActivity) response.messages = messages
response.messages = messages completion(response)
completion(response) }, error: { _ in
}, error: { _ in let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self))
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self)) let response = INSearchForMessagesIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity)
let response = INSearchForMessagesIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) completion(response)
completion(response) }))
}))
} }
// MARK: - INSetMessageAttributeIntentHandling // MARK: - INSetMessageAttributeIntentHandling
@ -408,101 +452,107 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
func handle(intent: INSetMessageAttributeIntent, completion: @escaping (INSetMessageAttributeIntentResponse) -> Void) { func handle(intent: INSetMessageAttributeIntent, completion: @escaping (INSetMessageAttributeIntentResponse) -> Void) {
self.actionDisposable.set((self.accountPromise.get() self.actionDisposable.set((self.accountPromise.get()
|> take(1) |> take(1)
|> mapError { _ -> IntentHandlingError in |> mapError { _ -> IntentHandlingError in
return .generic return .generic
}
|> mapToSignal { account -> Signal<Void, IntentHandlingError> in
guard let account = account else {
return .fail(.generic)
} }
|> mapToSignal { account -> Signal<Void, IntentHandlingError> in
var signals: [Signal<Void, IntentHandlingError>] = [] guard let account = account else {
var maxMessageIdsToApply: [PeerId: MessageId] = [:] return .fail(.generic)
if let identifiers = intent.identifiers { }
for identifier in identifiers {
let components = identifier.components(separatedBy: "_") var signals: [Signal<Void, IntentHandlingError>] = []
if let first = components.first, let peerId = Int64(first), let namespace = Int32(components[1]), let id = Int32(components[2]) { var maxMessageIdsToApply: [PeerId: MessageId] = [:]
let peerId = PeerId(peerId) if let identifiers = intent.identifiers {
let messageId = MessageId(peerId: peerId, namespace: namespace, id: id) for identifier in identifiers {
if let currentMessageId = maxMessageIdsToApply[peerId] { let components = identifier.components(separatedBy: "_")
if currentMessageId < messageId { if let first = components.first, let peerId = Int64(first), let namespace = Int32(components[1]), let id = Int32(components[2]) {
let peerId = PeerId(peerId)
let messageId = MessageId(peerId: peerId, namespace: namespace, id: id)
if let currentMessageId = maxMessageIdsToApply[peerId] {
if currentMessageId < messageId {
maxMessageIdsToApply[peerId] = messageId
}
} else {
maxMessageIdsToApply[peerId] = messageId maxMessageIdsToApply[peerId] = messageId
} }
} else {
maxMessageIdsToApply[peerId] = messageId
} }
} }
} }
}
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))
|> introduceError(IntentHandlingError.self)) }
}
if signals.isEmpty {
if signals.isEmpty {
return .complete()
} else {
account.shouldBeServiceTaskMaster.set(.single(.now))
return combineLatest(signals)
|> mapToSignal { _ -> Signal<Void, IntentHandlingError> in
return .complete() return .complete()
} } else {
|> afterDisposed { account.shouldBeServiceTaskMaster.set(.single(.now))
account.shouldBeServiceTaskMaster.set(.single(.never)) return combineLatest(signals)
|> mapToSignal { _ -> Signal<Void, IntentHandlingError> in
return .complete()
}
|> afterDisposed {
account.shouldBeServiceTaskMaster.set(.single(.never))
}
} }
} }
} |> deliverOnMainQueue).start(error: { _ in
|> deliverOnMainQueue).start(error: { _ in let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self))
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self)) let response = INSetMessageAttributeIntentResponse(code: .failure, userActivity: userActivity)
let response = INSetMessageAttributeIntentResponse(code: .failure, userActivity: userActivity) completion(response)
completion(response) }, completed: {
}, completed: { let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self))
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self)) let response = INSetMessageAttributeIntentResponse(code: .success, userActivity: userActivity)
let response = INSetMessageAttributeIntentResponse(code: .success, userActivity: userActivity) completion(response)
completion(response) }))
}))
} }
// MARK: - INStartAudioCallIntentHandling // MARK: - INStartAudioCallIntentHandling
func resolveContacts(for intent: INStartAudioCallIntent, with completion: @escaping ([INPersonResolutionResult]) -> Void) { func resolveContacts(for intent: INStartAudioCallIntent, with completion: @escaping ([INPersonResolutionResult]) -> Void) {
self.resolve(persons: intent.contacts, with: completion) guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
completion([INPersonResolutionResult.notRequired()])
return
}
self.resolve(persons: intent.contacts, with: { result in
completion(result.map { $0.personResolutionResult })
})
} }
func handle(intent: INStartAudioCallIntent, completion: @escaping (INStartAudioCallIntentResponse) -> Void) { func handle(intent: INStartAudioCallIntent, completion: @escaping (INStartAudioCallIntentResponse) -> Void) {
self.actionDisposable.set((self.accountPromise.get() self.actionDisposable.set((self.accountPromise.get()
|> take(1) |> take(1)
|> mapError { _ -> IntentHandlingError in |> mapError { _ -> IntentHandlingError in
return .generic return .generic
}
|> mapToSignal { account -> Signal<PeerId, IntentHandlingError> in
guard let contact = intent.contacts?.first, let customIdentifier = contact.customIdentifier, customIdentifier.hasPrefix("tg") else {
return .fail(.generic)
} }
|> mapToSignal { account -> Signal<PeerId, IntentHandlingError> in
guard let peerIdValue = Int64(String(customIdentifier[customIdentifier.index(customIdentifier.startIndex, offsetBy: 2)...])) else { guard let contact = intent.contacts?.first, let customIdentifier = contact.customIdentifier, customIdentifier.hasPrefix("tg") else {
return .fail(.generic) return .fail(.generic)
}
guard let peerIdValue = Int64(String(customIdentifier[customIdentifier.index(customIdentifier.startIndex, offsetBy: 2)...])) else {
return .fail(.generic)
}
let peerId = PeerId(peerIdValue)
if peerId.namespace != Namespaces.Peer.CloudUser {
return .fail(.generic)
}
return .single(peerId)
} }
|> deliverOnMainQueue).start(next: { peerId in
let peerId = PeerId(peerIdValue) let userActivity = NSUserActivity(activityType: NSStringFromClass(INStartAudioCallIntent.self))
if peerId.namespace != Namespaces.Peer.CloudUser { userActivity.userInfo = ["handle": "TGCA\(peerId.toInt64())"]
return .fail(.generic) let response = INStartAudioCallIntentResponse(code: .continueInApp, userActivity: userActivity)
} completion(response)
}, error: { _ in
return .single(peerId) let userActivity = NSUserActivity(activityType: NSStringFromClass(INStartAudioCallIntent.self))
} let response = INStartAudioCallIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity)
|> deliverOnMainQueue).start(next: { peerId in completion(response)
let userActivity = NSUserActivity(activityType: NSStringFromClass(INStartAudioCallIntent.self)) }))
userActivity.userInfo = ["handle": "TGCA\(peerId.toInt64())"]
let response = INStartAudioCallIntentResponse(code: .continueInApp, userActivity: userActivity)
completion(response)
}, error: { _ in
let userActivity = NSUserActivity(activityType: NSStringFromClass(INStartAudioCallIntent.self))
let response = INStartAudioCallIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity)
completion(response)
}))
} }
// MARK: - INSearchCallHistoryIntentHandling // MARK: - INSearchCallHistoryIntentHandling
@ -518,34 +568,34 @@ 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)
|> mapToSignal { account -> Signal<[CallRecord], IntentHandlingError> in
guard let account = account else {
return .fail(.generic)
}
account.shouldBeServiceTaskMaster.set(.single(.now))
return missedCalls(account: account)
|> introduceError(IntentHandlingError.self) |> introduceError(IntentHandlingError.self)
|> afterDisposed { |> mapToSignal { account -> Signal<[CallRecord], IntentHandlingError> in
account.shouldBeServiceTaskMaster.set(.single(.never)) guard let account = account else {
return .fail(.generic)
}
account.shouldBeServiceTaskMaster.set(.single(.now))
return missedCalls(account: account)
|> introduceError(IntentHandlingError.self)
|> afterDisposed {
account.shouldBeServiceTaskMaster.set(.single(.never))
}
} }
} |> deliverOnMainQueue).start(next: { calls in
|> deliverOnMainQueue).start(next: { calls in let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchCallHistoryIntent.self))
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchCallHistoryIntent.self)) let response: INSearchCallHistoryIntentResponse
let response: INSearchCallHistoryIntentResponse if #available(iOSApplicationExtension 11.0, *) {
if #available(iOSApplicationExtension 11.0, *) { response = INSearchCallHistoryIntentResponse(code: .success, userActivity: userActivity)
response = INSearchCallHistoryIntentResponse(code: .success, userActivity: userActivity) response.callRecords = calls.map { $0.intentCall }
response.callRecords = calls.map { $0.intentCall } } else {
} else { response = INSearchCallHistoryIntentResponse(code: .continueInApp, userActivity: userActivity)
response = INSearchCallHistoryIntentResponse(code: .continueInApp, userActivity: userActivity) }
} completion(response)
completion(response) }, error: { _ in
}, error: { _ in let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchCallHistoryIntent.self))
let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchCallHistoryIntent.self)) let response = INSearchCallHistoryIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity)
let response = INSearchCallHistoryIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) completion(response)
completion(response) }))
}))
} }
} }

View File

@ -95,14 +95,17 @@ final class AlertControllerNode: ASDisplayNode {
self.bottomDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.bottomDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.leftDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.leftDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.rightDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.rightDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { [weak self] _ in self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { [weak self] finished in
self?.centerDimView.backgroundColor = nil if finished {
self?.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: UIColor(white: 0.0, alpha: 0.5)) self?.centerDimView.backgroundColor = nil
self?.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: UIColor(white: 0.0, alpha: 0.5))
}
}) })
self.containerNode.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil) self.containerNode.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil)
} }
func animateOut(completion: @escaping () -> Void) { func animateOut(completion: @escaping () -> Void) {
self.containerNode.layer.removeAllAnimations()
self.centerDimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.centerDimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.centerDimView.image = nil self.centerDimView.image = nil

View File

@ -126,7 +126,7 @@ private final class AnimatedStickerCachedFrameSource: AnimatedStickerFrameSource
self.queue = queue self.queue = queue
self.data = data self.data = data
self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE)) self.scratchBuffer = Data(count: compression_decode_scratch_buffer_size(COMPRESSION_LZFSE))
var offset = 0 var offset = 0
var width = 0 var width = 0
var height = 0 var height = 0
@ -403,7 +403,7 @@ final class AnimatedStickerNode: ASDisplayNode {
self.renderer?.frame = CGRect(origin: CGPoint(), size: self.bounds.size) self.renderer?.frame = CGRect(origin: CGPoint(), size: self.bounds.size)
self.addSubnode(self.renderer!) self.addSubnode(self.renderer!)
} }
func setup(account: Account, resource: MediaResource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode = .loop, mode: AnimatedStickerMode) { func setup(account: Account, resource: MediaResource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode = .loop, mode: AnimatedStickerMode) {
if width < 2 || height < 2 { if width < 2 || height < 2 {
return return
@ -412,27 +412,27 @@ final class AnimatedStickerNode: ASDisplayNode {
switch mode { switch mode {
case .direct: case .direct:
self.disposable.set((account.postbox.mediaBox.resourceData(resource) self.disposable.set((account.postbox.mediaBox.resourceData(resource)
|> deliverOnMainQueue).start(next: { [weak self] data in |> deliverOnMainQueue).start(next: { [weak self] data in
guard let strongSelf = self, data.complete else { guard let strongSelf = self, data.complete else {
return return
} }
if let directData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [.mappedRead]) { if let directData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [.mappedRead]) {
strongSelf.directData = Tuple(directData, data.path, width, height) strongSelf.directData = Tuple(directData, data.path, width, height)
} }
if strongSelf.isPlaying {
strongSelf.play()
}
}))
case .cached:
self.disposable.set((chatMessageAnimationData(postbox: account.postbox, resource: resource, width: width, height: height, synchronousLoad: false)
|> deliverOnMainQueue).start(next: { [weak self] data in
if let strongSelf = self, data.complete {
strongSelf.cachedData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [.mappedRead])
if strongSelf.isPlaying { if strongSelf.isPlaying {
strongSelf.play() strongSelf.play()
} }
} }))
})) case .cached:
self.disposable.set((chatMessageAnimationData(postbox: account.postbox, resource: resource, width: width, height: height, synchronousLoad: false)
|> deliverOnMainQueue).start(next: { [weak self] data in
if let strongSelf = self, data.complete {
strongSelf.cachedData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [.mappedRead])
if strongSelf.isPlaying {
strongSelf.play()
}
}
}))
} }
} }
@ -565,7 +565,7 @@ final class AnimatedStickerNode: ASDisplayNode {
strongSelf.started() strongSelf.started()
} }
}) })
strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: false, duration: duration, timestamp: 0.0))) strongSelf.playbackStatus.set(.single(AnimatedStickerStatus(playing: false, duration: duration, timestamp: 0.0)))
} }
} }

View File

@ -1121,13 +1121,13 @@ final class SharedApplicationContext {
Logger.shared.log("App \(self.episodeId)", "isActive = \(value)") Logger.shared.log("App \(self.episodeId)", "isActive = \(value)")
}) })
/*if let url = launchOptions?[.url] { if let url = launchOptions?[.url] {
if let url = url as? URL, url.scheme == "tg" { if let url = url as? URL, url.scheme == "tg" {
self.openUrlWhenReady(url: url.absoluteString) self.openUrlWhenReady(url: url.absoluteString)
} else if let url = url as? String, url.lowercased().hasPrefix("tg://") { } else if let url = url as? String, url.lowercased().hasPrefix("tg://") {
self.openUrlWhenReady(url: url) self.openUrlWhenReady(url: url)
} }
}*/ }
if application.applicationState == .active { if application.applicationState == .active {
self.isInForegroundValue = true self.isInForegroundValue = true
@ -1845,7 +1845,7 @@ final class SharedApplicationContext {
notificationCenter.getNotificationSettings(completionHandler: { settings in notificationCenter.getNotificationSettings(completionHandler: { settings in
switch (settings.authorizationStatus, authorize) { switch (settings.authorizationStatus, authorize) {
case (.authorized, _), (.notDetermined, true): case (.authorized, _), (.notDetermined, true):
notificationCenter.requestAuthorization(options: [.badge, .sound, .alert, .carPlay], completionHandler: { result, _ in notificationCenter.requestAuthorization(options: [.badge, .sound, .alert], completionHandler: { result, _ in
completion(result) completion(result)
if result { if result {
Queue.mainQueue().async { Queue.mainQueue().async {
@ -1867,7 +1867,7 @@ final class SharedApplicationContext {
} }
var carPlayOptions = options var carPlayOptions = options
carPlayOptions.insert(.allowInCarPlay) //carPlayOptions.insert(.allowInCarPlay)
unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options) unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
replyMessageCategory = UNNotificationCategory(identifier: "withReply", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions) replyMessageCategory = UNNotificationCategory(identifier: "withReply", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
@ -1878,7 +1878,7 @@ final class SharedApplicationContext {
muteMessageCategory = UNNotificationCategory(identifier: "withMute", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options) muteMessageCategory = UNNotificationCategory(identifier: "withMute", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
muteMediaMessageCategory = UNNotificationCategory(identifier: "withMuteMedia", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options) muteMediaMessageCategory = UNNotificationCategory(identifier: "withMuteMedia", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
} else { } else {
let carPlayOptions: UNNotificationCategoryOptions = [.allowInCarPlay] let carPlayOptions: UNNotificationCategoryOptions = [] //[.allowInCarPlay]
unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], options: []) unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], options: [])
replyMessageCategory = UNNotificationCategory(identifier: "withReply", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], options: carPlayOptions) replyMessageCategory = UNNotificationCategory(identifier: "withReply", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], options: carPlayOptions)

View File

@ -15,7 +15,7 @@ private func timerValueString(days: Int32, hours: Int32, minutes: Int32, color:
var hoursString = "" var hoursString = ""
if hours > 0 || days > 0 { if hours > 0 || days > 0 {
daysString = strings.MessageTimer_Hours(hours) + " " hoursString = strings.MessageTimer_Hours(hours) + " "
} }
let minutesString = strings.MessageTimer_Minutes(minutes) let minutesString = strings.MessageTimer_Minutes(minutes)

View File

@ -81,13 +81,13 @@ typedef enum {
NSDictionary *outputSettings = @ NSDictionary *outputSettings = @
{ {
AVFormatIDKey: @(kAudioFormatLinearPCM), AVFormatIDKey: @(kAudioFormatLinearPCM),
AVSampleRateKey: @(TGBridgeAudioEncoderSampleRate), AVSampleRateKey: @(TGBridgeAudioEncoderSampleRate),
AVNumberOfChannelsKey: @1, AVNumberOfChannelsKey: @1,
AVLinearPCMBitDepthKey: @16, AVLinearPCMBitDepthKey: @16,
AVLinearPCMIsFloatKey: @false, AVLinearPCMIsFloatKey: @false,
AVLinearPCMIsBigEndianKey: @false, AVLinearPCMIsBigEndianKey: @false,
AVLinearPCMIsNonInterleaved: @false AVLinearPCMIsNonInterleaved: @false
}; };
_readerOutput = [AVAssetReaderAudioMixOutput assetReaderAudioMixOutputWithAudioTracks:asset.tracks audioSettings:outputSettings]; _readerOutput = [AVAssetReaderAudioMixOutput assetReaderAudioMixOutputWithAudioTracks:asset.tracks audioSettings:outputSettings];
@ -114,9 +114,9 @@ typedef enum {
static ATQueue *queue = nil; static ATQueue *queue = nil;
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^ dispatch_once(&onceToken, ^
{ {
queue = [[ATQueue alloc] initWithName:@"org.telegram.opusAudioEncoderQueue"]; queue = [[ATQueue alloc] initWithName:@"org.telegram.opusAudioEncoderQueue"];
}); });
return queue; return queue;
} }
@ -124,76 +124,76 @@ typedef enum {
static const int encoderPacketSizeInBytes = TGBridgeAudioEncoderSampleRate / 1000 * 60 * 2; static const int encoderPacketSizeInBytes = TGBridgeAudioEncoderSampleRate / 1000 * 60 * 2;
- (void)startWithCompletion:(void (^)(NSString *, int32_t))completion - (void)startWithCompletion:(void (^)(NSString *, int32_t))completion
{ {
[[TGBridgeAudioEncoder processingQueue] dispatch:^ [[TGBridgeAudioEncoder processingQueue] dispatch:^
{ {
_oggWriter = [[TGOggOpusWriter alloc] init]; _oggWriter = [[TGOggOpusWriter alloc] init];
if (![_oggWriter beginWithDataItem:_tempFileItem]) if (![_oggWriter beginWithDataItem:_tempFileItem])
{ {
[self cleanup]; [self cleanup];
return; return;
} }
[_assetReader startReading]; [_assetReader startReading];
while (_assetReader.status != AVAssetReaderStatusCompleted) while (_assetReader.status != AVAssetReaderStatusCompleted)
{ {
if (_assetReader.status == AVAssetReaderStatusReading) if (_assetReader.status == AVAssetReaderStatusReading)
{ {
CMSampleBufferRef nextBuffer = [_readerOutput copyNextSampleBuffer]; CMSampleBufferRef nextBuffer = [_readerOutput copyNextSampleBuffer];
if (nextBuffer) if (nextBuffer)
{ {
AudioBufferList abl; AudioBufferList abl;
CMBlockBufferRef blockBuffer; CMBlockBufferRef blockBuffer;
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(nextBuffer, NULL, &abl, sizeof(abl), NULL, NULL, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer); CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(nextBuffer, NULL, &abl, sizeof(abl), NULL, NULL, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, &blockBuffer);
[[TGBridgeAudioEncoder processingQueue] dispatch:^ [[TGBridgeAudioEncoder processingQueue] dispatch:^
{ {
[self _processBuffer:&abl.mBuffers[0]]; [self _processBuffer:&abl.mBuffers[0]];
CFRelease(nextBuffer); CFRelease(nextBuffer);
CFRelease(blockBuffer); CFRelease(blockBuffer);
}]; }];
} }
else else
{ {
[[TGBridgeAudioEncoder processingQueue] dispatch:^ [[TGBridgeAudioEncoder processingQueue] dispatch:^
{ {
if (_tailLength > 0) { if (_tailLength > 0) {
[_oggWriter writeFrame:(uint8_t *)_audioBuffer.bytes frameByteCount:(NSUInteger)_tailLength]; [_oggWriter writeFrame:(uint8_t *)_audioBuffer.bytes frameByteCount:(NSUInteger)_tailLength];
} }
}]; }];
break; break;
} }
} }
} }
[[TGBridgeAudioEncoder processingQueue] dispatch:^ [[TGBridgeAudioEncoder processingQueue] dispatch:^
{ {
TGFileDataItem *dataItemResult = nil; TGFileDataItem *dataItemResult = nil;
NSTimeInterval durationResult = 0.0; NSTimeInterval durationResult = 0.0;
NSUInteger totalBytes = 0; NSUInteger totalBytes = 0;
if (_assetReader.status == AVAssetReaderStatusCompleted) if (_assetReader.status == AVAssetReaderStatusCompleted)
{ {
NSLog(@"finished"); NSLog(@"finished");
if (_oggWriter != nil) if (_oggWriter != nil)
{ {
dataItemResult = _tempFileItem; dataItemResult = _tempFileItem;
durationResult = [_oggWriter encodedDuration]; durationResult = [_oggWriter encodedDuration];
totalBytes = [_oggWriter encodedBytes]; totalBytes = [_oggWriter encodedBytes];
} }
[self cleanup]; [self cleanup];
} }
//TGLog(@"[TGBridgeAudioEncoder#%x convert time: %f ms]", self, (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0); //TGLog(@"[TGBridgeAudioEncoder#%x convert time: %f ms]", self, (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0);
if (completion != nil) if (completion != nil)
completion(dataItemResult.path, (int32_t)durationResult); completion(dataItemResult.path, (int32_t)durationResult);
}]; }];
}]; }];
} }
- (void)_processBuffer:(AudioBuffer const *)buffer - (void)_processBuffer:(AudioBuffer const *)buffer
@ -270,7 +270,7 @@ static const int encoderPacketSizeInBytes = TGBridgeAudioEncoderSampleRate / 100
@implementation TGFileDataItem @implementation TGFileDataItem
{ {
ATQueue *_queue; ATQueue *_queue;
} }
- (void)_commonInit - (void)_commonInit
@ -306,11 +306,11 @@ static const int encoderPacketSizeInBytes = TGBridgeAudioEncoderSampleRate / 100
[_queue dispatch:^ [_queue dispatch:^
{ {
_fileName = filePath; _fileName = filePath;
_length = [[[NSFileManager defaultManager] attributesOfItemAtPath:_fileName error:nil][NSFileSize] unsignedIntegerValue]; _length = [[[NSFileManager defaultManager] attributesOfItemAtPath:_fileName error:nil][NSFileSize] unsignedIntegerValue];
_fileExists = [[NSFileManager defaultManager] fileExistsAtPath:_fileName]; _fileExists = [[NSFileManager defaultManager] fileExistsAtPath:_fileName];
}]; }];
} }
return self; return self;
} }
@ -322,38 +322,38 @@ static const int encoderPacketSizeInBytes = TGBridgeAudioEncoderSampleRate / 100
- (void)moveToPath:(NSString *)path - (void)moveToPath:(NSString *)path
{ {
[_queue dispatch:^ [_queue dispatch:^
{ {
[[NSFileManager defaultManager] moveItemAtPath:_fileName toPath:path error:nil]; [[NSFileManager defaultManager] moveItemAtPath:_fileName toPath:path error:nil];
_fileName = path; _fileName = path;
}]; }];
} }
- (void)remove - (void)remove
{ {
[_queue dispatch:^ [_queue dispatch:^
{ {
[[NSFileManager defaultManager] removeItemAtPath:_fileName error:nil]; [[NSFileManager defaultManager] removeItemAtPath:_fileName error:nil];
}]; }];
} }
- (void)appendData:(NSData *)data - (void)appendData:(NSData *)data
{ {
[_queue dispatch:^ [_queue dispatch:^
{ {
if (!_fileExists) if (!_fileExists)
{ {
[[NSFileManager defaultManager] createFileAtPath:_fileName contents:nil attributes:nil]; [[NSFileManager defaultManager] createFileAtPath:_fileName contents:nil attributes:nil];
_fileExists = true; _fileExists = true;
} }
NSFileHandle *file = [NSFileHandle fileHandleForUpdatingAtPath:_fileName]; NSFileHandle *file = [NSFileHandle fileHandleForUpdatingAtPath:_fileName];
[file seekToEndOfFile]; [file seekToEndOfFile];
[file writeData:data]; [file writeData:data];
[file synchronizeFile]; [file synchronizeFile];
[file closeFile]; [file closeFile];
_length += data.length; _length += data.length;
[_data appendData:data]; [_data appendData:data];
}]; }];
} }
- (NSData *)readDataAtOffset:(NSUInteger)offset length:(NSUInteger)length - (NSData *)readDataAtOffset:(NSUInteger)offset length:(NSUInteger)length
@ -361,14 +361,14 @@ static const int encoderPacketSizeInBytes = TGBridgeAudioEncoderSampleRate / 100
__block NSData *data = nil; __block NSData *data = nil;
[_queue dispatch:^ [_queue dispatch:^
{ {
NSFileHandle *file = [NSFileHandle fileHandleForUpdatingAtPath:_fileName]; NSFileHandle *file = [NSFileHandle fileHandleForUpdatingAtPath:_fileName];
[file seekToFileOffset:(unsigned long long)offset]; [file seekToFileOffset:(unsigned long long)offset];
data = [file readDataOfLength:length]; data = [file readDataOfLength:length];
if (data.length != length) if (data.length != length)
//TGLog(@"Read data length mismatch"); //TGLog(@"Read data length mismatch");
[file closeFile]; [file closeFile];
} synchronous:true]; } synchronous:true];
return data; return data;
} }
@ -377,9 +377,9 @@ static const int encoderPacketSizeInBytes = TGBridgeAudioEncoderSampleRate / 100
{ {
__block NSUInteger result = 0; __block NSUInteger result = 0;
[_queue dispatch:^ [_queue dispatch:^
{ {
result = _length; result = _length;
} synchronous:true]; } synchronous:true];
return result; return result;
} }
@ -420,11 +420,11 @@ static const int encoderPacketSizeInBytes = TGBridgeAudioEncoderSampleRate / 100
static ATQueue *queue = nil; static ATQueue *queue = nil;
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^ dispatch_once(&onceToken, ^
{ {
queue = [[ATQueue alloc] init]; queue = [[ATQueue alloc] init];
queue->_nativeQueue = dispatch_get_main_queue(); queue->_nativeQueue = dispatch_get_main_queue();
queue->_isMainQueue = true; queue->_isMainQueue = true;
}); });
return queue; return queue;
} }
@ -434,9 +434,9 @@ static const int encoderPacketSizeInBytes = TGBridgeAudioEncoderSampleRate / 100
static ATQueue *queue = nil; static ATQueue *queue = nil;
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^ dispatch_once(&onceToken, ^
{ {
queue = [[ATQueue alloc] initWithNativeQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; queue = [[ATQueue alloc] initWithNativeQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
}); });
return queue; return queue;
} }
@ -446,9 +446,9 @@ static const int encoderPacketSizeInBytes = TGBridgeAudioEncoderSampleRate / 100
static ATQueue *queue = nil; static ATQueue *queue = nil;
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^ dispatch_once(&onceToken, ^
{ {
queue = [[ATQueue alloc] initWithNativeQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)]; queue = [[ATQueue alloc] initWithNativeQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)];
}); });
return queue; return queue;
} }

View File

@ -846,6 +846,14 @@ public class ChatListController: TelegramBaseController, UIViewControllerPreview
} }
} }
self.chatListDisplayNode.isEmptyUpdated = { [weak self] isEmpty in
if let strongSelf = self, let searchContentNode = strongSelf.searchContentNode, let validLayout = strongSelf.validLayout {
if isEmpty {
searchContentNode.updateListVisibleContentOffset(.known(0.0))
}
}
}
self.chatListDisplayNode.toolbarActionSelected = { [weak self] action in self.chatListDisplayNode.toolbarActionSelected = { [weak self] action in
self?.toolbarActionSelected(action: action) self?.toolbarActionSelected(action: action)
} }

View File

@ -63,7 +63,8 @@ final class ChatListControllerNode: ASDisplayNode {
var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)? var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)?
var requestAddContact: ((String) -> Void)? var requestAddContact: ((String) -> Void)?
var dismissSelf: (() -> Void)? var dismissSelf: (() -> Void)?
var isEmptyUpdated: ((Bool) -> Void)?
let debugListView = ListView() let debugListView = ListView()
init(context: AccountContext, groupId: PeerGroupId, controlsHistoryPreload: Bool, presentationData: PresentationData, controller: ChatListController) { init(context: AccountContext, groupId: PeerGroupId, controlsHistoryPreload: Bool, presentationData: PresentationData, controller: ChatListController) {
@ -99,6 +100,7 @@ final class ChatListControllerNode: ASDisplayNode {
if let (layout, navigationHeight, visualNavigationHeight) = strongSelf.containerLayout { if let (layout, navigationHeight, visualNavigationHeight) = strongSelf.containerLayout {
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, visualNavigationHeight: visualNavigationHeight, transition: .immediate) strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, visualNavigationHeight: visualNavigationHeight, transition: .immediate)
} }
strongSelf.isEmptyUpdated?(true)
} }
case .notEmpty(false): case .notEmpty(false):
if case .group = strongSelf.groupId { if case .group = strongSelf.groupId {

View File

@ -56,7 +56,7 @@ private class ChatMessageHeartbeatHaptic {
if time > 2.0 { if time > 2.0 {
return return
} }
var startTime: Double = 0.0 var startTime: Double = 0.0
var delay: Double = 0.0 var delay: Double = 0.0
@ -201,7 +201,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} }
self.view.addGestureRecognizer(replyRecognizer) self.view.addGestureRecognizer(replyRecognizer)
} }
override var visibility: ListViewItemNodeVisibility { override var visibility: ListViewItemNodeVisibility {
didSet { didSet {
let wasVisible = oldValue != .none let wasVisible = oldValue != .none
@ -237,7 +237,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
break break
} }
} }
if self.telegramFile == nil, let emojiFile = item.associatedData.animatedEmojiStickers[item.message.text.trimmedEmoji]?.file { if self.telegramFile == nil, let emojiFile = item.associatedData.animatedEmojiStickers[item.message.text.trimmedEmoji]?.file {
if self.emojiFile?.id != emojiFile.id { if self.emojiFile?.id != emojiFile.id {
self.emojiFile = emojiFile self.emojiFile = emojiFile
@ -341,23 +341,23 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
var hasAvatar = false var hasAvatar = false
switch item.chatLocation { switch item.chatLocation {
case let .peer(peerId): case let .peer(peerId):
if peerId != item.context.account.peerId { if peerId != item.context.account.peerId {
if peerId.isGroupOrChannel && item.message.author != nil { if peerId.isGroupOrChannel && item.message.author != nil {
var isBroadcastChannel = false var isBroadcastChannel = false
if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info { if let peer = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
isBroadcastChannel = true isBroadcastChannel = true
} }
if !isBroadcastChannel { if !isBroadcastChannel {
hasAvatar = true hasAvatar = true
}
} }
} else if incoming {
hasAvatar = true
} }
} else if incoming {
hasAvatar = true
}
/*case .group: /*case .group:
hasAvatar = true*/ hasAvatar = true*/
} }
if hasAvatar { if hasAvatar {
@ -441,7 +441,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
statusType = .FreeOutgoing(.Sent(read: item.read)) statusType = .FreeOutgoing(.Sent(read: item.read))
} }
} }
var viewCount: Int? = nil var viewCount: Int? = nil
for attribute in item.message.attributes { for attribute in item.message.attributes {
if let attribute = attribute as? ViewCountMessageAttribute { if let attribute = attribute as? ViewCountMessageAttribute {
@ -704,128 +704,128 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state { switch recognizer.state {
case .ended: case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture { switch gesture {
case .tap: case .tap:
if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) { if let avatarNode = self.accessoryItemNode as? ChatMessageAvatarAccessoryItemNode, avatarNode.frame.contains(location) {
if let item = self.item, let author = item.content.firstMessage.author { if let item = self.item, let author = item.content.firstMessage.author {
var openPeerId = item.effectiveAuthorId ?? author.id var openPeerId = item.effectiveAuthorId ?? author.id
var navigate: ChatControllerInteractionNavigateToPeer var navigate: ChatControllerInteractionNavigateToPeer
if item.content.firstMessage.id.peerId == item.context.account.peerId { if item.content.firstMessage.id.peerId == item.context.account.peerId {
navigate = .chat(textInputState: nil, messageId: nil) navigate = .chat(textInputState: nil, messageId: nil)
} else {
navigate = .info
}
for attribute in item.content.firstMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
openPeerId = attribute.messageId.peerId
navigate = .chat(textInputState: nil, messageId: attribute.messageId)
}
}
if item.effectiveAuthorId?.namespace == Namespaces.Peer.Empty {
item.controllerInteraction.displayMessageTooltip(item.content.firstMessage.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, avatarNode.frame)
} else {
if let channel = item.content.firstMessage.forwardInfo?.author as? TelegramChannel, channel.username == nil {
if case .member = channel.participationStatus {
} else { } else {
navigate = .info item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, self, avatarNode.frame)
} return
for attribute in item.content.firstMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
openPeerId = attribute.messageId.peerId
navigate = .chat(textInputState: nil, messageId: attribute.messageId)
}
}
if item.effectiveAuthorId?.namespace == Namespaces.Peer.Empty {
item.controllerInteraction.displayMessageTooltip(item.content.firstMessage.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, self, avatarNode.frame)
} else {
if let channel = item.content.firstMessage.forwardInfo?.author as? TelegramChannel, channel.username == nil {
if case .member = channel.participationStatus {
} else {
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_PrivateChannelTooltip, self, avatarNode.frame)
return
}
}
item.controllerInteraction.openPeer(openPeerId, navigate, item.message)
} }
} }
return item.controllerInteraction.openPeer(openPeerId, navigate, item.message)
} }
}
if let viaBotNode = self.viaBotNode, viaBotNode.frame.contains(location) { return
if let item = self.item {
for attribute in item.message.attributes {
if let attribute = attribute as? InlineBotMessageAttribute {
var botAddressName: String?
if let peerId = attribute.peerId, let botPeer = item.message.peers[peerId], let addressName = botPeer.addressName {
botAddressName = addressName
} else {
botAddressName = attribute.title
}
if let botAddressName = botAddressName {
item.controllerInteraction.updateInputState { textInputState in
return ChatTextInputState(inputText: NSAttributedString(string: "@" + botAddressName + " "))
}
item.controllerInteraction.updateInputMode { _ in
return .text
}
}
return
}
}
}
}
if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) {
if let item = self.item {
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId)
return
}
}
}
}
if let item = self.item, self.imageNode.frame.contains(location) {
if self.telegramFile != nil {
let _ = item.controllerInteraction.openMessage(item.message, .default)
} else if let _ = self.emojiFile {
var startTime: Signal<Double, NoError>
if self.animationNode.playIfNeeded() {
startTime = .single(0.0)
} else {
startTime = self.animationNode.status
|> map { $0.timestamp }
|> take(1)
|> deliverOnMainQueue
}
if self.item?.message.text == "❤️" {
let _ = startTime.start(next: { [weak self] time in
guard let strongSelf = self else {
return
}
let heartbeatHaptic: ChatMessageHeartbeatHaptic
if let current = strongSelf.heartbeatHaptic {
heartbeatHaptic = current
} else {
heartbeatHaptic = ChatMessageHeartbeatHaptic()
heartbeatHaptic.enabled = true
strongSelf.heartbeatHaptic = heartbeatHaptic
}
if !heartbeatHaptic.active {
heartbeatHaptic.start(time: time)
}
})
}
}
return
}
self.item?.controllerInteraction.clickThroughMessage()
case .longTap, .doubleTap:
if let item = self.item, self.imageNode.frame.contains(location) {
item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame)
}
case .hold:
break
} }
if let viaBotNode = self.viaBotNode, viaBotNode.frame.contains(location) {
if let item = self.item {
for attribute in item.message.attributes {
if let attribute = attribute as? InlineBotMessageAttribute {
var botAddressName: String?
if let peerId = attribute.peerId, let botPeer = item.message.peers[peerId], let addressName = botPeer.addressName {
botAddressName = addressName
} else {
botAddressName = attribute.title
}
if let botAddressName = botAddressName {
item.controllerInteraction.updateInputState { textInputState in
return ChatTextInputState(inputText: NSAttributedString(string: "@" + botAddressName + " "))
}
item.controllerInteraction.updateInputMode { _ in
return .text
}
}
return
}
}
}
}
if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) {
if let item = self.item {
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId)
return
}
}
}
}
if let item = self.item, self.imageNode.frame.contains(location) {
if self.telegramFile != nil {
let _ = item.controllerInteraction.openMessage(item.message, .default)
} else if let _ = self.emojiFile {
var startTime: Signal<Double, NoError>
if self.animationNode.playIfNeeded() {
startTime = .single(0.0)
} else {
startTime = self.animationNode.status
|> map { $0.timestamp }
|> take(1)
|> deliverOnMainQueue
}
if self.item?.message.text == "❤️" {
let _ = startTime.start(next: { [weak self] time in
guard let strongSelf = self else {
return
}
let heartbeatHaptic: ChatMessageHeartbeatHaptic
if let current = strongSelf.heartbeatHaptic {
heartbeatHaptic = current
} else {
heartbeatHaptic = ChatMessageHeartbeatHaptic()
heartbeatHaptic.enabled = true
strongSelf.heartbeatHaptic = heartbeatHaptic
}
if !heartbeatHaptic.active {
heartbeatHaptic.start(time: time)
}
})
}
}
return
}
self.item?.controllerInteraction.clickThroughMessage()
case .longTap, .doubleTap:
if let item = self.item, self.imageNode.frame.contains(location) {
item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame)
}
case .hold:
break
} }
default: }
break default:
break
} }
} }

View File

@ -19,6 +19,8 @@ extension UnicodeScalar {
return true return true
case 0x1f004: case 0x1f004:
return true return true
case 0x2764:
return true
case 0x270b, 0x2728: case 0x270b, 0x2728:
return true return true
default: default:

View File

@ -279,7 +279,7 @@ final class ManagedAudioRecorderContext {
strongSelf.toneTimer?.invalidate() strongSelf.toneTimer?.invalidate()
} }
} }
}, queue: queue) }, queue: queue)
self.toneTimer = toneTimer self.toneTimer = toneTimer
toneTimer.start() toneTimer.start()
} else { } else {
@ -287,25 +287,25 @@ final class ManagedAudioRecorderContext {
} }
/*if beginWithTone, let beginToneData = beginToneData { /*if beginWithTone, let beginToneData = beginToneData {
self.tonePlayer = TonePlayer() self.tonePlayer = TonePlayer()
self.tonePlayer?.play(data: beginToneData, completed: { [weak self] in self.tonePlayer?.play(data: beginToneData, completed: { [weak self] in
queue.async { queue.async {
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
let toneTimer = SwiftSignalKit.Timer(timeout: 0.3, repeat: false, completion: { [weak self] in let toneTimer = SwiftSignalKit.Timer(timeout: 0.3, repeat: false, completion: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.processSamples = true strongSelf.processSamples = true
}, queue: queue) }, queue: queue)
strongSelf.toneTimer = toneTimer strongSelf.toneTimer = toneTimer
toneTimer.start() toneTimer.start()
} }
}) })
} else { } else {
self.processSamples = true self.processSamples = true
}*/ }*/
addAudioRecorderContext(self.id, self) addAudioRecorderContext(self.id, self)
addAudioUnitHolder(self.id, queue, self.audioUnit) addAudioUnitHolder(self.id, queue, self.audioUnit)
@ -314,7 +314,7 @@ final class ManagedAudioRecorderContext {
self.idleTimerExtensionDisposable = (Signal<Void, NoError> { subscriber in self.idleTimerExtensionDisposable = (Signal<Void, NoError> { subscriber in
return pushIdleTimerExtension() return pushIdleTimerExtension()
} |> delay(5.0, queue: queue)).start() } |> delay(5.0, queue: queue)).start()
} }
deinit { deinit {
@ -401,19 +401,19 @@ final class ManagedAudioRecorderContext {
strongSelf.audioSessionAcquired(headset: state.isHeadsetConnected) strongSelf.audioSessionAcquired(headset: state.isHeadsetConnected)
} }
} }
}, deactivate: { [weak self] in }, deactivate: { [weak self] in
return Signal { subscriber in return Signal { subscriber in
queue.async { queue.async {
if let strongSelf = self { if let strongSelf = self {
strongSelf.hasAudioSession = false strongSelf.hasAudioSession = false
strongSelf.stop() strongSelf.stop()
strongSelf.recordingState.set(.stopped) strongSelf.recordingState.set(.stopped)
subscriber.putCompletion() subscriber.putCompletion()
}
} }
return EmptyDisposable
} }
return EmptyDisposable
}
}) })
} }
} }

View File

@ -294,7 +294,9 @@ public class ShareRootControllerImpl {
} }
cancelImpl = { [weak shareController] in cancelImpl = { [weak shareController] in
shareController?.dismiss() shareController?.dismiss(completion: { [weak self] in
self?.getExtensionContext()?.completeRequest(returningItems: nil, completionHandler: nil)
})
} }
if let strongSelf = self { if let strongSelf = self {