diff --git a/submodules/AvatarNode/BUILD b/submodules/AvatarNode/BUILD index 2afd25db42..d0bc67136a 100644 --- a/submodules/AvatarNode/BUILD +++ b/submodules/AvatarNode/BUILD @@ -16,6 +16,9 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/AccountContext:AccountContext", "//submodules/Emoji:Emoji", + "//submodules/ImageCompression:ImageCompression", + "//submodules/TinyThumbnail:TinyThumbnail", + "//submodules/FastBlur:FastBlur", ], visibility = [ "//visibility:public", diff --git a/submodules/AvatarNode/Sources/AvatarNode.swift b/submodules/AvatarNode/Sources/AvatarNode.swift index 3278d2713a..3273da60bc 100644 --- a/submodules/AvatarNode/Sources/AvatarNode.swift +++ b/submodules/AvatarNode/Sources/AvatarNode.swift @@ -224,7 +224,7 @@ public final class AvatarNode: ASDisplayNode { public init(font: UIFont) { self.font = font - self.imageNode = ImageNode(enableHasImage: true) + self.imageNode = ImageNode(enableHasImage: true, enableAnimatedTransition: true) super.init() diff --git a/submodules/AvatarNode/Sources/PeerAvatar.swift b/submodules/AvatarNode/Sources/PeerAvatar.swift index 9fa6c87d07..5bb05e30b9 100644 --- a/submodules/AvatarNode/Sources/PeerAvatar.swift +++ b/submodules/AvatarNode/Sources/PeerAvatar.swift @@ -6,6 +6,9 @@ import Display import ImageIO import TelegramCore import SyncCore +import ImageCompression +import TinyThumbnail +import FastBlur private let roundCorners = { () -> UIImage in let diameter: CGFloat = 60.0 @@ -21,19 +24,32 @@ private let roundCorners = { () -> UIImage in return image }() -public func peerAvatarImageData(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, synchronousLoad: Bool) -> Signal? { +public enum PeerAvatarImageType { + case blurred + case complete +} + +public func peerAvatarImageData(account: Account, peerReference: PeerReference?, authorOfMessage: MessageReference?, representation: TelegramMediaImageRepresentation?, synchronousLoad: Bool) -> Signal<(Data, PeerAvatarImageType)?, NoError>? { if let smallProfileImage = representation { let resourceData = account.postbox.mediaBox.resourceData(smallProfileImage.resource, attemptSynchronously: synchronousLoad) let imageData = resourceData |> take(1) - |> mapToSignal { maybeData -> Signal in + |> mapToSignal { maybeData -> Signal<(Data, PeerAvatarImageType)?, NoError> in if maybeData.complete { - return .single(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) + if let data = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path)) { + return .single((data, .complete)) + } else { + return .single(nil) + } } else { return Signal { subscriber in let resourceDataDisposable = resourceData.start(next: { data in if data.complete { - subscriber.putNext(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) + if let dataValue = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path)) { + subscriber.putNext((dataValue, .complete)) + } else { + subscriber.putNext(nil) + } subscriber.putCompletion() } else { subscriber.putNext(nil) @@ -59,6 +75,24 @@ public func peerAvatarImageData(account: Account, peerReference: PeerReference?, } } return imageData + |> mapToSignal { data -> Signal<(Data, PeerAvatarImageType)?, NoError> in + guard let (dataValue, type) = data, case .complete = type else { + return .single(data) + } + + if let mappedImage = UIImage(data: dataValue), let miniData = compressImageMiniThumbnail(mappedImage, type: .avatar) { + //print("Demo avatar size: \(miniData.count) bytes") + if let decodedData = decodeTinyThumbnail(data: miniData) { + return Signal<(Data, PeerAvatarImageType)?, NoError>.single((decodedData, .blurred)) + |> then( + Signal<(Data, PeerAvatarImageType)?, NoError>.single((dataValue, .complete)) + |> delay(1.0, queue: .concurrentDefaultQueue()) + ) + } + } + + return .single(data) + } } else { return nil } @@ -100,8 +134,8 @@ public func peerAvatarImage(account: Account, peerReference: PeerReference?, aut return .single(nil) } let roundedImage = generateImage(displayDimensions, contextGenerator: { size, context -> Void in - if let data = data { - if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + if let (data, dataType) = data { + if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), var dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { context.clear(CGRect(origin: CGPoint(), size: displayDimensions)) context.setBlendMode(.copy) @@ -109,6 +143,18 @@ public func peerAvatarImage(account: Account, peerReference: PeerReference?, aut context.addEllipse(in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) context.clip() } + + if case .blurred = dataType { + let imageContextSize = CGSize(width: 64.0, height: 64.0) + let imageContext = DrawingContext(size: imageContextSize, scale: 1.0, premultiplied: true, clear: true) + imageContext.withFlippedContext { c in + c.draw(dataImage, in: CGRect(origin: CGPoint(), size: imageContextSize).insetBy(dx: inset, dy: inset)) + } + + telegramFastBlurMore(Int32(imageContext.size.width * imageContext.scale), Int32(imageContext.size.height * imageContext.scale), Int32(imageContext.bytesPerRow), imageContext.bytes) + + dataImage = imageContext.generateImage()!.cgImage! + } context.draw(dataImage, in: CGRect(origin: CGPoint(), size: displayDimensions).insetBy(dx: inset, dy: inset)) if round { @@ -141,7 +187,7 @@ public func peerAvatarImage(account: Account, peerReference: PeerReference?, aut let unroundedImage: UIImage? if provideUnrounded { unroundedImage = generateImage(displayDimensions, contextGenerator: { size, context -> Void in - if let data = data { + if let (data, _) = data { if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { context.clear(CGRect(origin: CGPoint(), size: displayDimensions)) context.setBlendMode(.copy) diff --git a/submodules/Display/Source/ImageNode.swift b/submodules/Display/Source/ImageNode.swift index 3522fad829..30cf507002 100644 --- a/submodules/Display/Source/ImageNode.swift +++ b/submodules/Display/Source/ImageNode.swift @@ -126,6 +126,7 @@ public class ImageNode: ASDisplayNode { private let hasImage: ValuePromise? private var first = true private let enableEmpty: Bool + private let enableAnimatedTransition: Bool private let _contentReady = Promise() private var didSetReady: Bool = false @@ -141,13 +142,14 @@ public class ImageNode: ASDisplayNode { } } - public init(enableHasImage: Bool = false, enableEmpty: Bool = false) { + public init(enableHasImage: Bool = false, enableEmpty: Bool = false, enableAnimatedTransition: Bool = false) { if enableHasImage { self.hasImage = ValuePromise(false, ignoreRepeated: true) } else { self.hasImage = nil } self.enableEmpty = enableEmpty + self.enableAnimatedTransition = enableAnimatedTransition super.init() } @@ -160,17 +162,33 @@ public class ImageNode: ASDisplayNode { self.disposable.set((signal |> deliverOnMainQueue).start(next: {[weak self] next in dispatcher.dispatch { if let strongSelf = self { - if let image = next?.cgImage { - strongSelf.contents = image - } else if strongSelf.enableEmpty { - strongSelf.contents = nil - } + var animate = strongSelf.enableAnimatedTransition if strongSelf.first && next != nil { strongSelf.first = false + animate = false if strongSelf.isNodeLoaded { strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) } } + if let image = next?.cgImage { + if animate, let previousContents = strongSelf.contents { + strongSelf.contents = image + let tempLayer = CALayer() + tempLayer.contents = previousContents + tempLayer.frame = strongSelf.layer.bounds + strongSelf.layer.addSublayer(tempLayer) + tempLayer.opacity = 0.0 + tempLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: true, completion: { [weak tempLayer] _ in + tempLayer?.removeFromSuperlayer() + }) + + //strongSelf.layer.animate(from: previousContents as! CGImage, to: image, keyPath: "contents", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2) + } else { + strongSelf.contents = image + } + } else if strongSelf.enableEmpty { + strongSelf.contents = nil + } if !reportedHasImage { if let hasImage = strongSelf.hasImage { reportedHasImage = true diff --git a/submodules/ImageCompression/Sources/ImageCompression.swift b/submodules/ImageCompression/Sources/ImageCompression.swift index 0d123acb2f..8988c56557 100644 --- a/submodules/ImageCompression/Sources/ImageCompression.swift +++ b/submodules/ImageCompression/Sources/ImageCompression.swift @@ -58,6 +58,31 @@ public func compressImage(_ image: UIImage, quality: Float) -> Data? { return data as Data } -public func compressImageMiniThumbnail(_ image: UIImage) -> Data? { - return compressMiniThumbnail(image) +public enum MiniThumbnailType { + case image + case avatar +} + +public func compressImageMiniThumbnail(_ image: UIImage, type: MiniThumbnailType = .image) -> Data? { + switch type { + case .image: + return compressMiniThumbnail(image, CGSize(width: 40.0, height: 40.0)) + case .avatar: + var size: CGFloat = 8.0 + var data = compressMiniThumbnail(image, CGSize(width: size, height: size)) + while true { + size += 1.0 + if let candidateData = compressMiniThumbnail(image, CGSize(width: size, height: size)) { + if candidateData.count >= 32 { + break + } else { + data = candidateData + } + } else { + break + } + } + + return data + } } diff --git a/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h b/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h index 3e7ec4696f..c7142e4e5e 100644 --- a/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h +++ b/submodules/MozjpegBinding/Public/MozjpegBinding/MozjpegBinding.h @@ -2,4 +2,4 @@ NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage); NSArray * _Nonnull extractJPEGDataScans(NSData * _Nonnull data); -NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image); +NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size); diff --git a/submodules/MozjpegBinding/Sources/MozjpegBinding.m b/submodules/MozjpegBinding/Sources/MozjpegBinding.m index fa2dd964e2..e2cdf62782 100644 --- a/submodules/MozjpegBinding/Sources/MozjpegBinding.m +++ b/submodules/MozjpegBinding/Sources/MozjpegBinding.m @@ -146,8 +146,7 @@ NSData * _Nullable compressJPEGData(UIImage * _Nonnull sourceImage) { return result; } -NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image) { - CGSize size = CGSizeMake(40.0f, 40.0f); +NSData * _Nullable compressMiniThumbnail(UIImage * _Nonnull image, CGSize size) { CGSize fittedSize = image.size; if (fittedSize.width > size.width) { fittedSize = CGSizeMake(size.width, (int)((fittedSize.height * size.width / MAX(fittedSize.width, 1.0f)))); diff --git a/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift b/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift index 0dff9dc0bc..bf734dd26c 100644 --- a/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift +++ b/submodules/TelegramCore/Sources/State/ManagedLocalInputActivities.swift @@ -143,7 +143,14 @@ private func requestActivity(postbox: Postbox, network: Network, accountPeerId: return .complete() } if let channel = peer as? TelegramChannel, case .broadcast = channel.info { - return .complete() + if let activity = activity { + switch activity { + case .speakingInGroupCall: + break + default: + return .complete() + } + } } if let _ = peer as? TelegramUser { if let presence = transaction.getPeerPresence(peerId: peerId) as? TelegramUserPresence { diff --git a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift index 33c95fe481..aa7ca83050 100644 --- a/submodules/WatchBridge/Sources/WatchRequestHandlers.swift +++ b/submodules/WatchBridge/Sources/WatchRequestHandlers.swift @@ -404,7 +404,7 @@ final class WatchMediaHandler: WatchRequestHandler { if let imageData = imageData { return imageData |> map { data -> UIImage? in - if let data = data, let image = generateImage(targetSize, contextGenerator: { size, context -> Void in + if let (data, _) = data, let image = generateImage(targetSize, contextGenerator: { size, context -> Void in if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { context.setBlendMode(.copy) context.draw(dataImage, in: CGRect(origin: CGPoint(), size: targetSize))