Merge branch 'beta' into experimental-2

Support more notification actions
This commit is contained in:
Ali 2021-09-17 21:06:58 +03:00
commit 1dfed86ea9
25 changed files with 2075 additions and 177 deletions

View File

@ -1 +1 @@
61c0e29ede9b63175583b4609216b9c6083192c87d0e6ee0a42a5ff263b627dc
61c0e29ede9b63175583b4609216b9c6083192c87d0e6ee0a42a5ff263b627dd

View File

@ -16,7 +16,10 @@ swift_library(
"//submodules/AppLockState:AppLockState",
"//submodules/NotificationsPresentationData:NotificationsPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/OpenSSLEncryptionProvider:OpenSSLEncryptionProvider"
"//submodules/OpenSSLEncryptionProvider:OpenSSLEncryptionProvider",
"//submodules/WebPBinding:WebPBinding",
"//submodules/rlottie:RLottieBinding",
"//submodules/GZip:GZip",
],
visibility = [
"//visibility:public",

View File

@ -6,6 +6,10 @@ import TelegramCore
import BuildConfig
import OpenSSLEncryptionProvider
import TelegramUIPreferences
import WebPBinding
import RLottieBinding
import GZip
import UIKit
private let queue = Queue()
@ -30,6 +34,249 @@ private func rootPathForBasePath(_ appGroupPath: String) -> String {
return appGroupPath + "/telegram-data"
}
private let deviceColorSpace: CGColorSpace = {
if #available(iOSApplicationExtension 9.3, iOS 9.3, *) {
if let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) {
return colorSpace
} else {
return CGColorSpaceCreateDeviceRGB()
}
} else {
return CGColorSpaceCreateDeviceRGB()
}
}()
private func getSharedDevideGraphicsContextSettings() -> DeviceGraphicsContextSettings {
struct OpaqueSettings {
let rowAlignment: Int
let bitsPerPixel: Int
let bitsPerComponent: Int
let opaqueBitmapInfo: CGBitmapInfo
let colorSpace: CGColorSpace
init(context: CGContext) {
self.rowAlignment = context.bytesPerRow
self.bitsPerPixel = context.bitsPerPixel
self.bitsPerComponent = context.bitsPerComponent
self.opaqueBitmapInfo = context.bitmapInfo
if #available(iOS 10.0, *) {
if UIScreen.main.traitCollection.displayGamut == .P3 {
self.colorSpace = CGColorSpace(name: CGColorSpace.displayP3) ?? context.colorSpace!
} else {
self.colorSpace = context.colorSpace!
}
} else {
self.colorSpace = context.colorSpace!
}
assert(self.rowAlignment == 32)
assert(self.bitsPerPixel == 32)
assert(self.bitsPerComponent == 8)
}
}
struct TransparentSettings {
let transparentBitmapInfo: CGBitmapInfo
init(context: CGContext) {
self.transparentBitmapInfo = context.bitmapInfo
}
}
var opaqueSettings: OpaqueSettings?
var transparentSettings: TransparentSettings?
if #available(iOS 10.0, *) {
let opaqueFormat = UIGraphicsImageRendererFormat()
let transparentFormat = UIGraphicsImageRendererFormat()
if #available(iOS 12.0, *) {
opaqueFormat.preferredRange = .standard
transparentFormat.preferredRange = .standard
}
opaqueFormat.opaque = true
transparentFormat.opaque = false
let opaqueRenderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), format: opaqueFormat)
let _ = opaqueRenderer.image(actions: { context in
opaqueSettings = OpaqueSettings(context: context.cgContext)
})
let transparentRenderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)), format: transparentFormat)
let _ = transparentRenderer.image(actions: { context in
transparentSettings = TransparentSettings(context: context.cgContext)
})
} else {
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1.0, height: 1.0), true, 1.0)
let refContext = UIGraphicsGetCurrentContext()!
opaqueSettings = OpaqueSettings(context: refContext)
UIGraphicsEndImageContext()
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1.0, height: 1.0), false, 1.0)
let refCtxTransparent = UIGraphicsGetCurrentContext()!
transparentSettings = TransparentSettings(context: refCtxTransparent)
UIGraphicsEndImageContext()
}
return DeviceGraphicsContextSettings(
rowAlignment: opaqueSettings!.rowAlignment,
bitsPerPixel: opaqueSettings!.bitsPerPixel,
bitsPerComponent: opaqueSettings!.bitsPerComponent,
opaqueBitmapInfo: opaqueSettings!.opaqueBitmapInfo,
transparentBitmapInfo: transparentSettings!.transparentBitmapInfo,
colorSpace: opaqueSettings!.colorSpace
)
}
public struct DeviceGraphicsContextSettings {
public static let shared: DeviceGraphicsContextSettings = getSharedDevideGraphicsContextSettings()
public let rowAlignment: Int
public let bitsPerPixel: Int
public let bitsPerComponent: Int
public let opaqueBitmapInfo: CGBitmapInfo
public let transparentBitmapInfo: CGBitmapInfo
public let colorSpace: CGColorSpace
public func bytesPerRow(forWidth width: Int) -> Int {
let baseValue = self.bitsPerPixel * width / 8
return (baseValue + 31) & ~0x1F
}
}
private final class DrawingContext {
let size: CGSize
let scale: CGFloat
let scaledSize: CGSize
let bytesPerRow: Int
private let bitmapInfo: CGBitmapInfo
let length: Int
let bytes: UnsafeMutableRawPointer
private let data: Data
private let context: CGContext
private var hasGeneratedImage = false
func withContext(_ f: (CGContext) -> ()) {
let context = self.context
context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0)
f(context)
context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0)
}
func withFlippedContext(_ f: (CGContext) -> ()) {
f(self.context)
}
init(size: CGSize, scale: CGFloat = 1.0, opaque: Bool = false, clear: Bool = false) {
assert(!size.width.isZero && !size.height.isZero)
let size: CGSize = CGSize(width: max(1.0, size.width), height: max(1.0, size.height))
let actualScale: CGFloat
if scale.isZero {
actualScale = 1.0
} else {
actualScale = scale
}
self.size = size
self.scale = actualScale
self.scaledSize = CGSize(width: size.width * actualScale, height: size.height * actualScale)
self.bytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: Int(self.scaledSize.width))
self.length = self.bytesPerRow * Int(self.scaledSize.height)
self.bytes = malloc(self.length)
self.data = Data(bytesNoCopy: self.bytes, count: self.length, deallocator: .custom({ bytes, _ in
free(bytes)
}))
if opaque {
self.bitmapInfo = DeviceGraphicsContextSettings.shared.opaqueBitmapInfo
} else {
self.bitmapInfo = DeviceGraphicsContextSettings.shared.transparentBitmapInfo
}
self.context = CGContext(
data: self.bytes,
width: Int(self.scaledSize.width),
height: Int(self.scaledSize.height),
bitsPerComponent: 8,
bytesPerRow: self.bytesPerRow,
space: deviceColorSpace,
bitmapInfo: self.bitmapInfo.rawValue,
releaseCallback: nil,
releaseInfo: nil
)!
self.context.scaleBy(x: self.scale, y: self.scale)
if clear {
memset(self.bytes, 0, self.length)
}
}
func generateImage() -> UIImage? {
if self.scaledSize.width.isZero || self.scaledSize.height.isZero {
return nil
}
if self.hasGeneratedImage {
preconditionFailure()
}
self.hasGeneratedImage = true
guard let dataProvider = CGDataProvider(data: self.data as CFData) else {
return nil
}
if let image = CGImage(
width: Int(self.scaledSize.width),
height: Int(self.scaledSize.height),
bitsPerComponent: self.context.bitsPerComponent,
bitsPerPixel: self.context.bitsPerPixel,
bytesPerRow: self.context.bytesPerRow,
space: DeviceGraphicsContextSettings.shared.colorSpace,
bitmapInfo: self.context.bitmapInfo,
provider: dataProvider,
decode: nil,
shouldInterpolate: true,
intent: .defaultIntent
) {
return UIImage(cgImage: image, scale: self.scale, orientation: .up)
} else {
return nil
}
}
}
private extension CGSize {
func fitted(_ size: CGSize) -> CGSize {
var fittedSize = self
if fittedSize.width > size.width {
fittedSize = CGSize(width: size.width, height: floor((fittedSize.height * size.width / max(fittedSize.width, 1.0))))
}
if fittedSize.height > size.height {
fittedSize = CGSize(width: floor((fittedSize.width * size.height / max(fittedSize.height, 1.0))), height: size.height)
}
return fittedSize
}
}
private func convertLottieImage(data: Data) -> UIImage? {
let decompressedData = TGGUnzipData(data, 512 * 1024) ?? data
guard let animation = LottieInstance(data: decompressedData, cacheKey: "") else {
return nil
}
let size = animation.dimensions.fitted(CGSize(width: 200.0, height: 200.0))
let context = DrawingContext(size: size, scale: 1.0, opaque: false, clear: true)
animation.renderFrame(with: 0, into: context.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(context.scaledSize.width), height: Int32(context.scaledSize.height), bytesPerRow: Int32(context.bytesPerRow))
return context.generateImage()
}
@available(iOSApplicationExtension 10.0, iOS 10.0, *)
private struct NotificationContent {
var title: String?
@ -40,31 +287,38 @@ private struct NotificationContent {
var badge: Int?
var category: String?
var userInfo: [AnyHashable: Any] = [:]
var attachments: [UNNotificationAttachment] = []
func asNotificationContent() -> UNNotificationContent {
let content = UNMutableNotificationContent()
content.title = self.title ?? ""
content.subtitle = self.subtitle ?? ""
content.body = self.body ?? ""
if let title = self.title {
content.title = title
}
if let subtitle = self.subtitle {
content.subtitle = subtitle
}
if let body = self.body {
content.body = body
}
if let threadId = self.threadId {
content.threadIdentifier = threadId
}
if let sound = self.sound {
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: sound))
}
if let badge = self.badge {
content.badge = badge as NSNumber
}
if let category = self.category {
content.categoryIdentifier = category
}
content.userInfo = self.userInfo
if !self.userInfo.isEmpty {
content.userInfo = self.userInfo
}
if !self.attachments.isEmpty {
content.attachments = self.attachments
}
return content
}
@ -175,146 +429,441 @@ private final class NotificationServiceHandler {
completed()
return
}
guard let aps = payloadJson["aps"] as? [String: Any] else {
completed()
return
}
var content: NotificationContent = NotificationContent()
if let alert = aps["alert"] as? [String: Any] {
content.title = alert["title"] as? String
content.subtitle = alert["subtitle"] as? String
content.body = alert["body"] as? String
} else if let alert = aps["alert"] as? String {
content.body = alert
} else {
completed()
return
}
var peerId: PeerId?
var messageId: MessageId.Id?
var mediaAttachment: Media?
if let messageIdString = payloadJson["msg_id"] as? String {
content.userInfo["msg_id"] = messageIdString
messageId = Int32(messageIdString)
}
if let fromIdString = payloadJson["from_id"] as? String {
content.userInfo["from_id"] = fromIdString
if let userIdValue = Int64(fromIdString) {
peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userIdValue))
}
} else if let chatIdString = payloadJson["chat_id"] as? String {
content.userInfo["chat_id"] = chatIdString
if let chatIdValue = Int64(chatIdString) {
peerId = PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(chatIdValue))
}
} else if let channelIdString = payloadJson["channel_id"] as? String {
content.userInfo["channel_id"] = channelIdString
if let channelIdValue = Int64(channelIdString) {
peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelIdValue))
}
}
if let silentString = payloadJson["silent"] as? String {
if let silentValue = Int(silentString), silentValue != 0 {
if let title = content.title {
content.title = "\(title) 🔕"
enum Action {
case logout
case poll(peerId: PeerId, content: NotificationContent)
case deleteMessage([MessageId])
case readMessage(MessageId)
}
var action: Action?
if let locKey = payloadJson["loc-key"] as? String {
switch locKey {
case "SESSION_REVOKE":
action = .logout
case "MESSAGE_MUTED":
if let peerId = peerId {
action = .poll(peerId: peerId, content: NotificationContent())
}
}
}
if let threadId = aps["thread-id"] as? String {
content.threadId = threadId
}
if let sound = aps["sound"] as? String {
content.sound = sound
}
if let category = aps["category"] as? String {
content.category = category
let _ = messageId
/*if (peerId != 0 && messageId != 0 && parsedAttachment != nil && attachmentData != nil) {
userInfo[@"peerId"] = @(peerId);
userInfo[@"messageId.namespace"] = @(0);
userInfo[@"messageId.id"] = @(messageId);
userInfo[@"media"] = [attachmentData base64EncodedStringWithOptions:0];
if (isExpandableMedia) {
if ([categoryString isEqualToString:@"r"]) {
_bestAttemptContent.categoryIdentifier = @"withReplyMedia";
} else if ([categoryString isEqualToString:@"m"]) {
_bestAttemptContent.categoryIdentifier = @"withMuteMedia";
case "MESSAGE_DELETED":
if let peerId = peerId {
if let messageId = messageId {
action = .deleteMessage([MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageId)])
} else if let messageIds = payloadJson["messages"] as? String {
var messagesDeleted: [MessageId] = []
for messageId in messageIds.split(separator: ",") {
if let messageIdValue = Int32(messageId) {
messagesDeleted.append(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: messageIdValue))
}
}
action = .deleteMessage(messagesDeleted)
}
}
}*/
}
/*if (accountInfos.accounts.count > 1) {
if (_bestAttemptContent.title.length != 0 && account.peerName.length != 0) {
_bestAttemptContent.title = [NSString stringWithFormat:@"%@ → %@", _bestAttemptContent.title, account.peerName];
}
}*/
updateCurrentContent(content.asNotificationContent())
if let stateManager = strongSelf.stateManager, let peerId = peerId {
let pollCompletion: () -> Void = {
queue.async {
guard let strongSelf = self, let stateManager = strongSelf.stateManager else {
completed()
return
}
let _ = (renderedTotalUnreadCount(
accountManager: strongSelf.accountManager,
postbox: stateManager.postbox
)
|> deliverOn(strongSelf.queue)).start(next: { value in
content.badge = Int(value.0)
updateCurrentContent(content.asNotificationContent())
completed()
})
}
}
stateManager.network.shouldKeepConnection.set(.single(true))
if peerId.namespace == Namespaces.Peer.CloudChannel {
strongSelf.pollDisposable.set(standalonePollChannelOnce(
postbox: stateManager.postbox,
network: stateManager.network,
peerId: peerId,
stateManager: stateManager
).start(completed: {
pollCompletion()
}))
} else {
enum ControlError {
case restart
}
let signal = stateManager.standalonePollDifference()
|> castError(ControlError.self)
|> mapToSignal { result -> Signal<Never, ControlError> in
if result {
return .complete()
} else {
return .fail(.restart)
case "READ_HISTORY":
if let peerId = peerId {
if let messageIdString = payloadJson["max_id"] as? String {
if let maxId = Int32(messageIdString) {
action = .readMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: maxId))
}
}
}
|> restartIfError
strongSelf.pollDisposable.set(signal.start(completed: {
pollCompletion()
}))
default:
break
}
} else {
if let aps = payloadJson["aps"] as? [String: Any], let peerId = peerId {
var content: NotificationContent = NotificationContent()
if let alert = aps["alert"] as? [String: Any] {
content.title = alert["title"] as? String
content.subtitle = alert["subtitle"] as? String
content.body = alert["body"] as? String
} else if let alert = aps["alert"] as? String {
content.body = alert
} else {
completed()
return
}
if let messageId = messageId {
content.userInfo["msg_id"] = "\(messageId)"
}
if peerId.namespace == Namespaces.Peer.CloudUser {
content.userInfo["from_id"] = "\(peerId.id._internalGetInt64Value())"
} else if peerId.namespace == Namespaces.Peer.CloudGroup {
content.userInfo["chat_id"] = "\(peerId.id._internalGetInt64Value())"
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
content.userInfo["channel_id"] = "\(peerId.id._internalGetInt64Value())"
}
content.userInfo["peerId"] = "\(peerId.toInt64())"
content.userInfo["accountId"] = "\(record.0.int64)"
if let silentString = payloadJson["silent"] as? String {
if let silentValue = Int(silentString), silentValue != 0 {
if let title = content.title {
content.title = "\(title) 🔕"
}
}
}
if var attachmentDataString = payloadJson["attachb64"] as? String {
attachmentDataString = attachmentDataString.replacingOccurrences(of: "-", with: "+")
attachmentDataString = attachmentDataString.replacingOccurrences(of: "_", with: "/")
while attachmentDataString.count % 4 != 0 {
attachmentDataString.append("=")
}
if let attachmentData = Data(base64Encoded: attachmentDataString) {
mediaAttachment = _internal_parseMediaAttachment(data: attachmentData)
}
}
if let threadId = aps["thread-id"] as? String {
content.threadId = threadId
}
if let sound = aps["sound"] as? String {
content.sound = sound
}
if let category = aps["category"] as? String {
content.category = category
let _ = messageId
/*if (peerId != 0 && messageId != 0 && parsedAttachment != nil && attachmentData != nil) {
userInfo[@"peerId"] = @(peerId);
userInfo[@"messageId.namespace"] = @(0);
userInfo[@"messageId.id"] = @(messageId);
userInfo[@"media"] = [attachmentData base64EncodedStringWithOptions:0];
if (isExpandableMedia) {
if ([categoryString isEqualToString:@"r"]) {
_bestAttemptContent.categoryIdentifier = @"withReplyMedia";
} else if ([categoryString isEqualToString:@"m"]) {
_bestAttemptContent.categoryIdentifier = @"withMuteMedia";
}
}
}*/
}
/*if (accountInfos.accounts.count > 1) {
if (_bestAttemptContent.title.length != 0 && account.peerName.length != 0) {
_bestAttemptContent.title = [NSString stringWithFormat:@"%@ → %@", _bestAttemptContent.title, account.peerName];
}
}*/
action = .poll(peerId: peerId, content: content)
updateCurrentContent(content.asNotificationContent())
}
}
if let action = action {
switch action {
case .logout:
completed()
case .poll(let peerId, var content):
if let stateManager = strongSelf.stateManager {
let pollCompletion: () -> Void = {
queue.async {
guard let strongSelf = self, let stateManager = strongSelf.stateManager else {
completed()
return
}
var fetchMediaSignal: Signal<Data?, NoError> = .single(nil)
if let mediaAttachment = mediaAttachment {
var fetchResource: TelegramMultipartFetchableResource?
if let image = mediaAttachment as? TelegramMediaImage, let representation = largestImageRepresentation(image.representations), let resource = representation.resource as? TelegramMultipartFetchableResource {
fetchResource = resource
} else if let file = mediaAttachment as? TelegramMediaFile {
if file.isSticker {
fetchResource = file.resource as? TelegramMultipartFetchableResource
} else if file.isVideo {
fetchResource = file.previewRepresentations.first?.resource as? TelegramMultipartFetchableResource
}
}
if let resource = fetchResource {
if let _ = strongSelf.stateManager?.postbox.mediaBox.completedResourcePath(resource) {
} else {
let intervals: Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError> = .single([(0 ..< Int(Int32.max), MediaBoxFetchPriority.maximum)])
fetchMediaSignal = Signal { subscriber in
let collectedData = Atomic<Data>(value: Data())
return standaloneMultipartFetch(
postbox: stateManager.postbox,
network: stateManager.network,
resource: resource,
datacenterId: resource.datacenterId,
size: nil,
intervals: intervals,
parameters: MediaResourceFetchParameters(
tag: nil,
info: resourceFetchInfo(resource: resource),
isRandomAccessAllowed: true
),
encryptionKey: nil,
decryptedSize: nil,
continueInBackground: false,
useMainConnection: true
).start(next: { result in
switch result {
case let .dataPart(_, data, _, _):
let _ = collectedData.modify { current in
var current = current
current.append(data)
return current
}
default:
break
}
}, error: { _ in
subscriber.putNext(nil)
subscriber.putCompletion()
}, completed: {
subscriber.putNext(collectedData.with({ $0 }))
subscriber.putCompletion()
})
}
}
}
}
let _ = (fetchMediaSignal
|> timeout(10.0, queue: queue, alternate: .single(nil))
|> deliverOn(queue)).start(next: { mediaData in
guard let strongSelf = self, let stateManager = strongSelf.stateManager else {
completed()
return
}
let _ = (renderedTotalUnreadCount(
accountManager: strongSelf.accountManager,
postbox: stateManager.postbox
)
|> deliverOn(strongSelf.queue)).start(next: { value in
guard let strongSelf = self, let stateManager = strongSelf.stateManager else {
completed()
return
}
content.badge = Int(value.0)
if let image = mediaAttachment as? TelegramMediaImage, let resource = largestImageRepresentation(image.representations)?.resource {
if let mediaData = mediaData {
stateManager.postbox.mediaBox.storeResourceData(resource.id, data: mediaData, synchronous: true)
}
if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(resource, pathExtension: "jpg") {
if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: storedPath), options: nil) {
content.attachments.append(attachment)
}
}
} else if let file = mediaAttachment as? TelegramMediaFile {
if file.isStaticSticker {
let resource = file.resource
if let mediaData = mediaData {
stateManager.postbox.mediaBox.storeResourceData(resource.id, data: mediaData, synchronous: true)
}
if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(resource) {
if let data = try? Data(contentsOf: URL(fileURLWithPath: storedPath)), let image = WebP.convert(fromWebP: data) {
let tempFile = TempBox.shared.tempFile(fileName: "image.png")
let _ = try? image.pngData()?.write(to: URL(fileURLWithPath: tempFile.path))
if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: tempFile.path), options: nil) {
content.attachments.append(attachment)
}
}
}
} else if file.isAnimatedSticker {
let resource = file.resource
if let mediaData = mediaData {
stateManager.postbox.mediaBox.storeResourceData(resource.id, data: mediaData, synchronous: true)
}
if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(resource) {
if let data = try? Data(contentsOf: URL(fileURLWithPath: storedPath)), let image = convertLottieImage(data: data) {
let tempFile = TempBox.shared.tempFile(fileName: "image.png")
let _ = try? image.pngData()?.write(to: URL(fileURLWithPath: tempFile.path))
if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: tempFile.path), options: nil) {
content.attachments.append(attachment)
}
}
}
} else if file.isVideo, let representation = file.previewRepresentations.first {
let resource = representation.resource
if let mediaData = mediaData {
stateManager.postbox.mediaBox.storeResourceData(resource.id, data: mediaData, synchronous: true)
}
if let storedPath = stateManager.postbox.mediaBox.completedResourcePath(resource, pathExtension: "jpg") {
if let attachment = try? UNNotificationAttachment(identifier: "image", url: URL(fileURLWithPath: storedPath), options: nil) {
content.attachments.append(attachment)
}
}
}
}
updateCurrentContent(content.asNotificationContent())
completed()
})
})
}
}
let pollSignal: Signal<Never, NoError>
stateManager.network.shouldKeepConnection.set(.single(true))
if peerId.namespace == Namespaces.Peer.CloudChannel {
pollSignal = standalonePollChannelOnce(
postbox: stateManager.postbox,
network: stateManager.network,
peerId: peerId,
stateManager: stateManager
)
} else {
enum ControlError {
case restart
}
let signal = stateManager.standalonePollDifference()
|> castError(ControlError.self)
|> mapToSignal { result -> Signal<Never, ControlError> in
if result {
return .complete()
} else {
return .fail(.restart)
}
}
|> restartIfError
pollSignal = signal
}
strongSelf.pollDisposable.set(pollSignal.start(completed: {
pollCompletion()
}))
} else {
completed()
}
case let .deleteMessage(ids):
let mediaBox = stateManager.postbox.mediaBox
let _ = (stateManager.postbox.transaction { transaction -> Void in
_internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: ids, deleteMedia: true)
}
|> deliverOn(strongSelf.queue)).start(completed: {
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
var removeIdentifiers: [String] = []
for notification in notifications {
if let peerIdString = notification.request.content.userInfo["peerId"] as? String, let peerIdValue = Int64(peerIdString), let messageIdString = notification.request.content.userInfo["msg_id"] as? String, let messageIdValue = Int32(messageIdString) {
for id in ids {
if PeerId(peerIdValue) == id.peerId && messageIdValue == id.id {
removeIdentifiers.append(notification.request.identifier)
}
}
}
}
let completeRemoval: () -> Void = {
guard let strongSelf = self else {
return
}
let _ = (renderedTotalUnreadCount(
accountManager: strongSelf.accountManager,
postbox: stateManager.postbox
)
|> deliverOn(strongSelf.queue)).start(next: { value in
var content = NotificationContent()
content.badge = Int(value.0)
updateCurrentContent(content.asNotificationContent())
completed()
})
}
if !removeIdentifiers.isEmpty {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: removeIdentifiers)
queue.after(1.0, {
completeRemoval()
})
} else {
completeRemoval()
}
})
})
case let .readMessage(id):
let _ = (stateManager.postbox.transaction { transaction -> Void in
transaction.applyIncomingReadMaxId(id)
}
|> deliverOn(strongSelf.queue)).start(completed: {
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
var removeIdentifiers: [String] = []
for notification in notifications {
if let peerIdString = notification.request.content.userInfo["peerId"] as? String, let peerIdValue = Int64(peerIdString), let messageIdString = notification.request.content.userInfo["msg_id"] as? String, let messageIdValue = Int32(messageIdString) {
if PeerId(peerIdValue) == id.peerId && messageIdValue <= id.id {
removeIdentifiers.append(notification.request.identifier)
}
}
}
let completeRemoval: () -> Void = {
guard let strongSelf = self else {
return
}
let _ = (renderedTotalUnreadCount(
accountManager: strongSelf.accountManager,
postbox: stateManager.postbox
)
|> deliverOn(strongSelf.queue)).start(next: { value in
var content = NotificationContent()
content.badge = Int(value.0)
updateCurrentContent(content.asNotificationContent())
completed()
})
}
if !removeIdentifiers.isEmpty {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: removeIdentifiers)
queue.after(1.0, {
completeRemoval()
})
} else {
completeRemoval()
}
})
})
}
} else {
let content = NotificationContent()
updateCurrentContent(content.asNotificationContent())
completed()
}
}))

View File

@ -95,6 +95,7 @@ python3 build-system/Make/Make.py \
build \
--configurationPath="$HOME/telegram-configuration" \
--buildNumber="$BUILD_NUMBER" \
--disableParallelSwiftmoduleGeneration \
--configuration="$APP_CONFIGURATION"
OUTPUT_PATH="build/artifacts"

View File

@ -191,6 +191,8 @@ public final class GradientBackgroundNode: ASDisplayNode {
self.parentNode = parentNode
super.init()
self.displaysAsynchronously = false
self.index = parentNode.cloneNodes.add(Weak<CloneNode>(self))
self.image = parentNode.dimmedImage

View File

@ -7,7 +7,7 @@ protocol TelegramCloudMediaResource: TelegramMediaResource {
func apiInputLocation(fileReference: Data?) -> Api.InputFileLocation?
}
protocol TelegramMultipartFetchableResource: TelegramMediaResource {
public protocol TelegramMultipartFetchableResource: TelegramMediaResource {
var datacenterId: Int { get }
}

View File

@ -83,6 +83,7 @@ private struct DownloadWrapper {
let datacenterId: Int32
let isCdn: Bool
let network: Network
let useMainConnection: Bool
func request<T>(_ data: (FunctionDescription, Buffer, DeserializeFunctionResponse<T>), tag: MediaResourceFetchTag?, continueInBackground: Bool) -> Signal<T, MTRpcError> {
let target: MultiplexedRequestTarget
@ -568,11 +569,12 @@ private final class MultipartFetchManager {
let postbox: Postbox
let network: Network
let revalidationContext: MediaReferenceRevalidationContext
let revalidationContext: MediaReferenceRevalidationContext?
let continueInBackground: Bool
let partReady: (Int, Data) -> Void
let reportCompleteSize: (Int) -> Void
private let useMainConnection: Bool
private var source: MultipartFetchSource
var fetchingParts: [Int: (Int, Disposable)] = [:]
@ -591,10 +593,11 @@ private final class MultipartFetchManager {
var rangesDisposable: Disposable?
init(resource: TelegramMediaResource, parameters: MediaResourceFetchParameters?, size: Int?, intervals: Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>, encryptionKey: SecretFileEncryptionKey?, decryptedSize: Int32?, location: MultipartFetchMasterLocation, postbox: Postbox, network: Network, revalidationContext: MediaReferenceRevalidationContext, partReady: @escaping (Int, Data) -> Void, reportCompleteSize: @escaping (Int) -> Void) {
init(resource: TelegramMediaResource, parameters: MediaResourceFetchParameters?, size: Int?, intervals: Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>, encryptionKey: SecretFileEncryptionKey?, decryptedSize: Int32?, location: MultipartFetchMasterLocation, postbox: Postbox, network: Network, revalidationContext: MediaReferenceRevalidationContext?, partReady: @escaping (Int, Data) -> Void, reportCompleteSize: @escaping (Int) -> Void, useMainConnection: Bool) {
self.resource = resource
self.parameters = parameters
self.consumerId = Int64.random(in: Int64.min ... Int64.max)
self.useMainConnection = useMainConnection
self.completeSize = size
if let size = size {
@ -643,7 +646,7 @@ private final class MultipartFetchManager {
self.postbox = postbox
self.network = network
self.revalidationContext = revalidationContext
self.source = .master(location: location, download: DownloadWrapper(consumerId: self.consumerId, datacenterId: location.datacenterId, isCdn: false, network: network))
self.source = .master(location: location, download: DownloadWrapper(consumerId: self.consumerId, datacenterId: location.datacenterId, isCdn: false, network: network, useMainConnection: self.useMainConnection))
self.partReady = partReady
self.reportCompleteSize = reportCompleteSize
@ -820,8 +823,8 @@ private final class MultipartFetchManager {
case .revalidateMediaReference:
if !strongSelf.revalidatingMediaReference && !strongSelf.revalidatedMediaReference {
strongSelf.revalidatingMediaReference = true
if let info = strongSelf.parameters?.info as? TelegramCloudMediaResourceFetchInfo {
strongSelf.revalidateMediaReferenceDisposable.set((revalidateMediaResourceReference(postbox: strongSelf.postbox, network: strongSelf.network, revalidationContext: strongSelf.revalidationContext, info: info, resource: strongSelf.resource)
if let info = strongSelf.parameters?.info as? TelegramCloudMediaResourceFetchInfo, let revalidationContext = strongSelf.revalidationContext {
strongSelf.revalidateMediaReferenceDisposable.set((revalidateMediaResourceReference(postbox: strongSelf.postbox, network: strongSelf.network, revalidationContext: revalidationContext, info: info, resource: strongSelf.resource)
|> deliverOn(strongSelf.queue)).start(next: { validationResult in
if let strongSelf = self {
strongSelf.revalidatingMediaReference = false
@ -847,7 +850,7 @@ private final class MultipartFetchManager {
switch strongSelf.source {
case let .master(location, download):
strongSelf.partAlignment = Int(dataHashLength)
strongSelf.source = .cdn(masterDatacenterId: location.datacenterId, fileToken: token, key: key, iv: iv, download: DownloadWrapper(consumerId: strongSelf.consumerId, datacenterId: id, isCdn: true, network: strongSelf.network), masterDownload: download, hashSource: MultipartCdnHashSource(queue: strongSelf.queue, fileToken: token, hashes: partHashes, masterDownload: download, continueInBackground: strongSelf.continueInBackground))
strongSelf.source = .cdn(masterDatacenterId: location.datacenterId, fileToken: token, key: key, iv: iv, download: DownloadWrapper(consumerId: strongSelf.consumerId, datacenterId: id, isCdn: true, network: strongSelf.network, useMainConnection: strongSelf.useMainConnection), masterDownload: download, hashSource: MultipartCdnHashSource(queue: strongSelf.queue, fileToken: token, hashes: partHashes, masterDownload: download, continueInBackground: strongSelf.continueInBackground))
strongSelf.checkState()
case .cdn, .none:
break
@ -879,7 +882,29 @@ private final class MultipartFetchManager {
}
}
func multipartFetch(postbox: Postbox, network: Network, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, resource: TelegramMediaResource, datacenterId: Int, size: Int?, intervals: Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>, parameters: MediaResourceFetchParameters?, encryptionKey: SecretFileEncryptionKey? = nil, decryptedSize: Int32? = nil, continueInBackground: Bool = false) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
public func standaloneMultipartFetch(postbox: Postbox, network: Network, resource: TelegramMediaResource, datacenterId: Int, size: Int?, intervals: Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>, parameters: MediaResourceFetchParameters?, encryptionKey: SecretFileEncryptionKey? = nil, decryptedSize: Int32? = nil, continueInBackground: Bool = false, useMainConnection: Bool = false) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
return multipartFetch(
postbox: postbox,
network: network,
mediaReferenceRevalidationContext: nil,
resource: resource,
datacenterId: datacenterId,
size: size,
intervals: intervals,
parameters: parameters,
useMainConnection: useMainConnection
)
}
public func resourceFetchInfo(resource: TelegramMediaResource) -> MediaResourceFetchInfo? {
return TelegramCloudMediaResourceFetchInfo(
reference: MediaResourceReference.standalone(resource: resource),
preferBackgroundReferenceRevalidation: false,
continueInBackground: false
)
}
func multipartFetch(postbox: Postbox, network: Network, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext?, resource: TelegramMediaResource, datacenterId: Int, size: Int?, intervals: Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>, parameters: MediaResourceFetchParameters?, encryptionKey: SecretFileEncryptionKey? = nil, decryptedSize: Int32? = nil, continueInBackground: Bool = false, useMainConnection: Bool = false) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
return Signal { subscriber in
let location: MultipartFetchMasterLocation
if let resource = resource as? WebFileReferenceMediaResource {
@ -937,7 +962,7 @@ func multipartFetch(postbox: Postbox, network: Network, mediaReferenceRevalidati
}, reportCompleteSize: { size in
subscriber.putNext(.resourceSizeUpdated(size))
subscriber.putCompletion()
})
}, useMainConnection: useMainConnection)
manager.start()

View File

@ -195,7 +195,7 @@ public final class AccountStateManager {
self.appliedQtsDisposable.dispose()
var postbox: Postbox? = self.postbox
postbox?.queue.async {
postbox?.queue.after(0.5) {
postbox = nil
}
}

View File

@ -22,7 +22,7 @@ private final class MediaResourceDataCopyFile : MediaResourceDataFetchCopyLocalI
}
}
private func fetchCloudMediaLocation(account: Account, resource: TelegramMediaResource, datacenterId: Int, size: Int?, intervals: Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>, parameters: MediaResourceFetchParameters?) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
public func fetchCloudMediaLocation(account: Account, resource: TelegramMediaResource, datacenterId: Int, size: Int?, intervals: Signal<[(Range<Int>, MediaBoxFetchPriority)], NoError>, parameters: MediaResourceFetchParameters?) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
return multipartFetch(postbox: account.postbox, network: account.network, mediaReferenceRevalidationContext: account.mediaReferenceRevalidationContext, resource: resource, datacenterId: datacenterId, size: size, intervals: intervals, parameters: parameters)
}

View File

@ -336,6 +336,10 @@ final class PeerInputActivityManager {
timeout = 8.0
}
if activity == .choosingSticker {
context.removeActivity(peerId: peerId, activity: .typingText, episodeId: nil)
}
context.addActivity(peerId: peerId, activity: activity, timeout: timeout, episodeId: episodeId, nextUpdateId: &self.nextUpdateId)
if let globalContext = self.globalContext {

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 133
return 134
}
public func parseMessage(_ data: Data!) -> Any! {

View File

@ -28,9 +28,9 @@ public final class LoggingSettings: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: StringCodingKey.self)
try container.encode(self.logToFile ? 1 : 0, forKey: "logToFile")
try container.encode(self.logToConsole ? 1 : 0, forKey: "logToConsole")
try container.encode(self.redactSensitiveData ? 1 : 0, forKey: "redactSensitiveData")
try container.encode((self.logToFile ? 1 : 0) as Int32, forKey: "logToFile")
try container.encode((self.logToConsole ? 1 : 0) as Int32, forKey: "logToConsole")
try container.encode((self.redactSensitiveData ? 1 : 0) as Int32, forKey: "redactSensitiveData")
}
public func withUpdatedLogToFile(_ logToFile: Bool) -> LoggingSettings {

View File

@ -22,7 +22,7 @@ func addMessageMediaResourceIdsToRemove(message: Message, resourceIds: inout [Wr
}
}
func _internal_deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [MessageId], deleteMedia: Bool = true, manualAddMessageThreadStatsDifference: ((MessageId, Int, Int) -> Void)? = nil) {
public func _internal_deleteMessages(transaction: Transaction, mediaBox: MediaBox, ids: [MessageId], deleteMedia: Bool = true, manualAddMessageThreadStatsDifference: ((MessageId, Int, Int) -> Void)? = nil) {
var resourceIds: [WrappedMediaResourceId] = []
if deleteMedia {
for id in ids {

View File

@ -1,6 +1,6 @@
import Foundation
import Postbox
import TelegramApi
public extension MessageFlags {
var isSending: Bool {
@ -302,3 +302,15 @@ public extension Message {
}
}
public func _internal_parseMediaAttachment(data: Data) -> Media? {
guard let object = Api.parse(Buffer(buffer: MemoryBuffer(data: data))) else {
return nil
}
if let photo = object as? Api.Photo {
return telegramMediaImageFromApiPhoto(photo)
} else if let file = object as? Api.Document {
return telegramMediaFileFromApiDocument(file)
} else {
return nil
}
}

View File

@ -1270,6 +1270,7 @@ public final class PresentationTheme: Equatable {
public let inAppNotification: PresentationThemeInAppNotification
public let chart: PresentationThemeChart
public let preview: Bool
public var forceSync: Bool = false
public let resourceCache: PresentationsResourceCache = PresentationsResourceCache()

View File

@ -3889,6 +3889,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
|> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self {
if value {
strongSelf.context.account.updateLocalInputActivity(peerId: activitySpace, activity: .typingText, isPresent: false)
}
strongSelf.context.account.updateLocalInputActivity(peerId: activitySpace, activity: .choosingSticker, isPresent: value)
}
})
@ -3961,7 +3964,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
let customTheme = useDarkAppearance ? theme.darkTheme : theme.theme
if let settings = customTheme.settings, let theme = makePresentationTheme(settings: settings) {
theme.forceSync = true
presentationData = presentationData.withUpdated(theme: theme).withUpdated(chatWallpaper: theme.chat.defaultWallpaper)
Queue.mainQueue().after(1.0, {
theme.forceSync = false
})
}
} else if let darkAppearancePreview = darkAppearancePreview {
useDarkAppearance = darkAppearancePreview
@ -3985,7 +3993,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let themeSpecificWallpaper = themeSpecificWallpaper {
lightWallpaper = themeSpecificWallpaper
} else {
let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: themeSettings.theme, accentColor: currentColors?.color, bubbleColors: currentColors?.customBubbleColors ?? [], wallpaper: currentColors?.wallpaper, baseColor: currentColors?.baseColor) ?? defaultPresentationTheme
let theme = makePresentationTheme(mediaBox: accountManager.mediaBox, themeReference: themeSettings.theme, accentColor: currentColors?.color, bubbleColors: currentColors?.customBubbleColors ?? [], wallpaper: currentColors?.wallpaper, baseColor: currentColors?.baseColor, preview: true) ?? defaultPresentationTheme
lightWallpaper = theme.chat.defaultWallpaper
}
@ -4019,8 +4027,16 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if darkAppearancePreview {
darkTheme.forceSync = true
Queue.mainQueue().after(1.0, {
darkTheme.forceSync = false
})
presentationData = presentationData.withUpdated(theme: darkTheme).withUpdated(chatWallpaper: darkWallpaper)
} else {
lightTheme.forceSync = true
Queue.mainQueue().after(1.0, {
lightTheme.forceSync = false
})
presentationData = presentationData.withUpdated(theme: lightTheme).withUpdated(chatWallpaper: lightWallpaper)
}
}
@ -4037,7 +4053,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.presentationDataPromise.set(.single(strongSelf.presentationData))
if !isFirstTime && (previousThemeEmoticon?.0 != themeEmoticon || previousThemeEmoticon?.1 != useDarkAppearance) {
strongSelf.presentCrossfadeSnapshot(delay: 0.2)
strongSelf.presentCrossfadeSnapshot()
}
}
strongSelf.presentationReady.set(.single(true))
@ -13386,14 +13402,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
private var crossfading = false
private func presentCrossfadeSnapshot(delay: Double) {
private func presentCrossfadeSnapshot() {
guard !self.crossfading, let snapshotView = self.view.snapshotView(afterScreenUpdates: false) else {
return
}
self.crossfading = true
self.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: delay, removeOnCompletion: false, completion: { [weak self, weak snapshotView] _ in
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak self, weak snapshotView] _ in
self?.crossfading = false
snapshotView?.removeFromSuperview()
})
@ -13451,7 +13467,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let controller = ChatThemeScreen(context: context, updatedPresentationData: strongSelf.updatedPresentationData, animatedEmojiStickers: animatedEmojiStickers, initiallySelectedEmoticon: selectedEmoticon, peerName: strongSelf.presentationInterfaceState.renderedPeer?.chatMainPeer?.compactDisplayTitle ?? "", previewTheme: { [weak self] emoticon, dark in
if let strongSelf = self {
strongSelf.presentCrossfadeSnapshot(delay: 0.2)
strongSelf.presentCrossfadeSnapshot()
strongSelf.themeEmoticonAndDarkAppearancePreviewPromise.set(.single((emoticon, dark)))
}
}, completion: { [weak self] emoticon in

View File

@ -1122,6 +1122,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let rawTransition = preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, reverse: reverse, chatLocation: chatLocation, controllerInteraction: controllerInteraction, scrollPosition: updatedScrollPosition, scrollAnimationCurve: scrollAnimationCurve, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first, cachedData: initialData?.cachedData, cachedDataMessages: initialData?.cachedDataMessages, readStateData: initialData?.readStateData, flashIndicators: flashIndicators, updatedMessageSelection: previousSelectedMessages != selectedMessages, messageTransitionNode: messageTransitionNode(), allUpdated: updateAllOnEachVersion)
var mappedTransition = mappedChatHistoryViewListTransition(context: context, chatLocation: chatLocation, associatedData: associatedData, controllerInteraction: controllerInteraction, mode: mode, lastHeaderId: lastHeaderId, transition: rawTransition)
if disableAnimations {
mappedTransition.options.remove(.AnimateInsertion)
mappedTransition.options.remove(.AnimateAlpha)

View File

@ -523,7 +523,23 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
self.updateVisibility()
if let animationItems = item.associatedData.additionalAnimatedEmojiStickers[item.message.text.strippedEmoji] {
let textEmoji = item.message.text.strippedEmoji
var additionalTextEmoji = textEmoji
let (basicEmoji, fitz) = item.message.text.basicEmoji
if ["💛", "💙", "💚", "💜", "🧡", "🖤"].contains(textEmoji) {
additionalTextEmoji = "❤️".strippedEmoji
} else if fitz != nil {
additionalTextEmoji = basicEmoji
}
var animationItems: [Int: StickerPackItem]?
if let items = item.associatedData.additionalAnimatedEmojiStickers[item.message.text.strippedEmoji] {
animationItems = items
} else if let items = item.associatedData.additionalAnimatedEmojiStickers[additionalTextEmoji] {
animationItems = items
}
if let animationItems = animationItems {
for (_, animationItem) in animationItems {
self.disposables.add(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: animationItem.file)).start())
}
@ -1405,7 +1421,15 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
let textEmoji = item.message.text.strippedEmoji
guard let animationItems = item.associatedData.additionalAnimatedEmojiStickers[textEmoji], index < 10, let file = animationItems[index]?.file else {
var additionalTextEmoji = textEmoji
let (basicEmoji, fitz) = item.message.text.basicEmoji
if ["💛", "💙", "💚", "💜", "🧡", "🖤"].contains(textEmoji) {
additionalTextEmoji = "❤️".strippedEmoji
} else if fitz != nil {
additionalTextEmoji = basicEmoji
}
guard let animationItems = item.associatedData.additionalAnimatedEmojiStickers[additionalTextEmoji], index < 10, let file = animationItems[index]?.file else {
return
}
let source = AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: nil)
@ -1564,11 +1588,21 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
let text = item.message.text
if var firstScalar = text.unicodeScalars.first {
var textEmoji = text.strippedEmoji
let originalTextEmoji = textEmoji
var additionalTextEmoji = textEmoji
if beatingHearts.contains(firstScalar.value) {
textEmoji = "❤️"
firstScalar = UnicodeScalar(heart)!
}
let (basicEmoji, fitz) = text.basicEmoji
if ["💛", "💙", "💚", "💜", "🧡", "🖤", "❤️"].contains(textEmoji) {
additionalTextEmoji = "❤️".strippedEmoji
} else if fitz != nil {
additionalTextEmoji = basicEmoji
}
let syncAnimations = item.message.id.peerId.namespace == Namespaces.Peer.CloudUser
return .optionalAction({
var haptic: EmojiHaptic?
if let current = self.haptic {
@ -1585,8 +1619,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
self.haptic = haptic
}
if let animationItems = item.associatedData.additionalAnimatedEmojiStickers[originalTextEmoji] {
let syncAnimations = item.message.id.peerId.namespace == Namespaces.Peer.CloudUser
if syncAnimations, let animationItems = item.associatedData.additionalAnimatedEmojiStickers[additionalTextEmoji] {
let playHaptic = haptic == nil
var hapticFeedback: HapticFeedback

View File

@ -2323,6 +2323,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
backgroundType = .incoming(mergeType)
}
let hasWallpaper = item.presentationData.theme.wallpaper.hasWallpaper
if item.presentationData.theme.theme.forceSync {
transition = .immediate
}
strongSelf.backgroundNode.setType(type: backgroundType, highlighted: strongSelf.highlightedState, graphics: graphics, maskMode: strongSelf.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: transition, backgroundNode: presentationContext.backgroundNode)
strongSelf.backgroundWallpaperNode.setType(type: backgroundType, theme: item.presentationData.theme, essentialGraphics: graphics, maskMode: strongSelf.backgroundMaskMode, backgroundNode: presentationContext.backgroundNode)
strongSelf.shadowNode.setType(type: backgroundType, hasWallpaper: hasWallpaper, graphics: graphics)
@ -2375,7 +2378,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
strongSelf.clippingNode.addSubnode(nameNode)
}
nameNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY), size: nameNodeSizeApply.0)
nameNode.displaysAsynchronously = !item.presentationData.isPreview
nameNode.displaysAsynchronously = !item.presentationData.isPreview && !item.presentationData.theme.theme.forceSync
if let credibilityIconImage = currentCredibilityIconImage {
let credibilityIconNode: ASImageNode

View File

@ -381,7 +381,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
strongSelf.textNode.displaysAsynchronously = !item.presentationData.isPreview
strongSelf.textNode.displaysAsynchronously = !item.presentationData.isPreview && !item.presentationData.theme.theme.forceSync
let _ = textApply()
if let statusApply = statusApply, let adjustedStatusFrame = adjustedStatusFrame {

View File

@ -206,7 +206,7 @@ public final class ChatMessageTransitionNode: ASDisplayNode {
func updateLayout(size: CGSize) {
self.clippingNode.frame = CGRect(origin: CGPoint(), size: size)
let absoluteRect = self.itemNode.view.convert(self.itemNode.view.bounds, to: self.view)
let absoluteRect = self.itemNode.view.convert(self.itemNode.view.bounds, to: self.itemNode.supernode?.supernode?.view)
self.containerNode.frame = absoluteRect
}

View File

@ -165,8 +165,7 @@ private struct ThemeSettingsThemeItemNodeTransition {
private func ensureThemeVisible(listNode: ListView, emoticon: String?, animated: Bool) -> Bool {
var resultNode: ThemeSettingsThemeItemIconNode?
var previousNode: ThemeSettingsThemeItemIconNode?
let _ = previousNode
// var previousNode: ThemeSettingsThemeItemIconNode?
var nextNode: ThemeSettingsThemeItemIconNode?
listNode.forEachItemNode { node in
guard let node = node as? ThemeSettingsThemeItemIconNode else {
@ -176,7 +175,7 @@ private func ensureThemeVisible(listNode: ListView, emoticon: String?, animated:
if node.item?.emoticon == emoticon {
resultNode = node
} else {
previousNode = node
// previousNode = node
}
} else if nextNode == nil {
nextNode = node
@ -284,9 +283,11 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = false
self.emojiNode = TextNode()
self.emojiNode.isUserInteractionEnabled = false
self.emojiNode.displaysAsynchronously = false
self.emojiImageNode = TransformImageNode()
@ -496,7 +497,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
snapshotView.frame = self.containerNode.view.frame
self.view.insertSubview(snapshotView, aboveSubview: self.containerNode.view)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
@ -522,6 +523,9 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
}
final class ChatThemeScreen: ViewController {
static let themeCrossfadeDuration: Double = 0.3
static let themeCrossfadeDelay: Double = 0.25
private var controllerNode: ChatThemeScreenNode {
return self.displayNode as! ChatThemeScreenNode
}
@ -840,7 +844,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega
let action: (String?) -> Void = { [weak self] emoticon in
if let strongSelf = self, strongSelf.selectedEmoticon != emoticon {
strongSelf.animateCrossfade(animateIcon: false)
strongSelf.animateCrossfade(animateIcon: true)
strongSelf.previewTheme?(emoticon, strongSelf.isDarkAppearance)
strongSelf.selectedEmoticon = emoticon
@ -964,7 +968,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega
self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme))
if self.animationNode.isPlaying {
if let animationNode = self.animationNode.makeCopy(colors: iconColors(theme: self.presentationData.theme), progress: 0.25) {
if let animationNode = self.animationNode.makeCopy(colors: iconColors(theme: self.presentationData.theme), progress: 0.2) {
let previousAnimationNode = self.animationNode
self.animationNode = animationNode
@ -974,7 +978,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega
animationNode.isUserInteractionEnabled = false
animationNode.frame = previousAnimationNode.frame
previousAnimationNode.supernode?.insertSubnode(animationNode, belowSubnode: previousAnimationNode)
previousAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
previousAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, removeOnCompletion: false)
animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
} else {
@ -1010,6 +1014,11 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega
}
@objc func switchThemePressed() {
self.switchThemeButton.isUserInteractionEnabled = false
Queue.mainQueue().after(0.5) {
self.switchThemeButton.isUserInteractionEnabled = true
}
self.animateCrossfade(animateIcon: false)
self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme))
self.animationNode.playOnce()
@ -1025,21 +1034,19 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega
}
}
private func animateCrossfade(animateIcon: Bool = true) {
let delay: Double = 0.2
private func animateCrossfade(animateIcon: Bool) {
if animateIcon, let snapshotView = self.animationNode.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.animationNode.frame
self.animationNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.animationNode.view)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: delay, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
Queue.mainQueue().after(delay) {
Queue.mainQueue().after(ChatThemeScreen.themeCrossfadeDelay) {
if let effectView = self.effectNode.view as? UIVisualEffectView {
UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveEaseInOut) {
UIView.animate(withDuration: ChatThemeScreen.themeCrossfadeDuration, delay: 0.0, options: .curveLinear) {
effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark)
} completion: { _ in
}
@ -1047,14 +1054,14 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega
let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear
self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor
self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3)
self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: ChatThemeScreen.themeCrossfadeDuration)
}
if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.contentContainerNode.frame
self.contentContainerNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.contentContainerNode.view)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: delay, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: ChatThemeScreen.themeCrossfadeDuration, delay: ChatThemeScreen.themeCrossfadeDelay, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
@ -1068,8 +1075,6 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega
private var animatedOut = false
func animateIn() {
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY
let dimPosition = self.dimNode.layer.position

File diff suppressed because it is too large Load Diff

View File

@ -3477,16 +3477,19 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}
var mainItemsImpl: (() -> Signal<[ContextMenuItem], NoError>)?
mainItemsImpl = {
mainItemsImpl = { [weak self] in
var items: [ContextMenuItem] = []
guard let strongSelf = self else {
return .single(items)
}
let allHeaderButtons = Set(peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: false, videoCallsEnabled: self.videoCallsEnabled, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false))
let headerButtons = Set(peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: self.headerNode.isAvatarExpanded, videoCallsEnabled: self.videoCallsEnabled, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false))
let allHeaderButtons = Set(peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: strongSelf.isOpenedFromChat, isExpanded: false, videoCallsEnabled: strongSelf.videoCallsEnabled, isSecretChat: strongSelf.peerId.namespace == Namespaces.Peer.SecretChat, isContact: strongSelf.data?.isContact ?? false))
let headerButtons = Set(peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: strongSelf.isOpenedFromChat, isExpanded: strongSelf.headerNode.isAvatarExpanded, videoCallsEnabled: strongSelf.videoCallsEnabled, isSecretChat: strongSelf.peerId.namespace == Namespaces.Peer.SecretChat, isContact: strongSelf.data?.isContact ?? false))
let filteredButtons = allHeaderButtons.subtracting(headerButtons)
var canChangeColors = false
if peer is TelegramUser, self.data?.encryptionKeyFingerprint == nil {
if peer is TelegramUser, strongSelf.data?.encryptionKeyFingerprint == nil {
canChangeColors = true
}
@ -3626,7 +3629,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
})))
}
if self.peerId.namespace == Namespaces.Peer.CloudUser && user.botInfo == nil && !user.flags.contains(.isSupport) {
if strongSelf.peerId.namespace == Namespaces.Peer.CloudUser && user.botInfo == nil && !user.flags.contains(.isSupport) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_StartSecretChat, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Timer"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
@ -3646,7 +3649,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
})))
}
}
} else if self.peerId.namespace == Namespaces.Peer.SecretChat && data.isContact {
} else if strongSelf.peerId.namespace == Namespaces.Peer.SecretChat && data.isContact {
if let cachedData = data.cachedData as? CachedUserData, cachedData.isBlocked {
} else {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_BlockUser, icon: { theme in
@ -3659,7 +3662,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}
}
} else if let channel = peer as? TelegramChannel {
if let cachedData = self.data?.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) {
if let cachedData = strongSelf.data?.cachedData as? CachedChannelData, cachedData.flags.contains(.canViewStats) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.ChannelInfo_Stats, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in

View File

@ -65,6 +65,7 @@ public final class WallpaperBackgroundNode: ASDisplayNode {
self.bubbleType = bubbleType
self.contentNode = ASImageNode()
self.contentNode.displaysAsynchronously = false
self.contentNode.isUserInteractionEnabled = false
super.init()
@ -165,6 +166,7 @@ public final class WallpaperBackgroundNode: ASDisplayNode {
if needsWallpaperBackground {
if self.cleanWallpaperNode == nil {
let cleanWallpaperNode = ASImageNode()
cleanWallpaperNode.displaysAsynchronously = false
self.cleanWallpaperNode = cleanWallpaperNode
cleanWallpaperNode.frame = self.bounds
self.insertSubnode(cleanWallpaperNode, at: 0)