mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge branch 'beta' into experimental-2
Support more notification actions
This commit is contained in:
commit
1dfed86ea9
@ -1 +1 @@
|
||||
61c0e29ede9b63175583b4609216b9c6083192c87d0e6ee0a42a5ff263b627dc
|
||||
61c0e29ede9b63175583b4609216b9c6083192c87d0e6ee0a42a5ff263b627dd
|
||||
|
@ -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",
|
||||
|
@ -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()
|
||||
}
|
||||
}))
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -7,7 +7,7 @@ protocol TelegramCloudMediaResource: TelegramMediaResource {
|
||||
func apiInputLocation(fileReference: Data?) -> Api.InputFileLocation?
|
||||
}
|
||||
|
||||
protocol TelegramMultipartFetchableResource: TelegramMediaResource {
|
||||
public protocol TelegramMultipartFetchableResource: TelegramMediaResource {
|
||||
var datacenterId: Int { get }
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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! {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
1238
submodules/TelegramUI/Sources/ChatThemeScreen.swift.orig
Normal file
1238
submodules/TelegramUI/Sources/ChatThemeScreen.swift.orig
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user