diff --git a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift index 268671d2a2..8cc41a6eef 100644 --- a/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift +++ b/submodules/DirectMediaImageCache/Sources/DirectMediaImageCache.swift @@ -10,7 +10,47 @@ import MozjpegBinding import Accelerate import ManagedFile -private func generateBlurredThumbnail(image: UIImage) -> UIImage? { +private func adjustSaturationInContext(context: DrawingContext, saturation: CGFloat) { + var buffer = vImage_Buffer() + buffer.data = context.bytes + buffer.width = UInt(context.size.width * context.scale) + buffer.height = UInt(context.size.height * context.scale) + buffer.rowBytes = context.bytesPerRow + + let divisor: Int32 = 0x1000 + + let rwgt: CGFloat = 0.3086 + let gwgt: CGFloat = 0.6094 + let bwgt: CGFloat = 0.0820 + + let adjustSaturation = saturation + + let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation + let b = (1.0 - adjustSaturation) * rwgt + let c = (1.0 - adjustSaturation) * rwgt + let d = (1.0 - adjustSaturation) * gwgt + let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation + let f = (1.0 - adjustSaturation) * gwgt + let g = (1.0 - adjustSaturation) * bwgt + let h = (1.0 - adjustSaturation) * bwgt + let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation + + let satMatrix: [CGFloat] = [ + a, b, c, 0, + d, e, f, 0, + g, h, i, 0, + 0, 0, 0, 1 + ] + + var matrix: [Int16] = satMatrix.map { value in + return Int16(value * CGFloat(divisor)) + } + + vImageMatrixMultiply_ARGB8888(&buffer, &buffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) +} + + +private func generateBlurredThumbnail(image: UIImage, adjustSaturation: Bool = false) -> UIImage? { let thumbnailContextSize = CGSize(width: 32.0, height: 32.0) guard let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) else { return nil @@ -24,6 +64,10 @@ private func generateBlurredThumbnail(image: UIImage) -> UIImage? { } telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + if adjustSaturation { + adjustSaturationInContext(context: thumbnailContext, saturation: 1.7) + } + return thumbnailContext.generateImage() } @@ -158,10 +202,12 @@ private func loadImage(data: Data) -> UIImage? { public final class DirectMediaImageCache { public final class GetMediaResult { public let image: UIImage? + public let blurredImage: UIImage? public let loadSignal: Signal? - init(image: UIImage?, loadSignal: Signal?) { + init(image: UIImage?, blurredImage: UIImage? = nil, loadSignal: Signal?) { self.image = image + self.blurredImage = blurredImage self.loadSignal = loadSignal } } @@ -284,7 +330,7 @@ public final class DirectMediaImageCache { return self.getProgressiveSize(mediaReference: MediaReference.message(message: MessageReference(message), media: file).abstract, width: width, representations: file.previewRepresentations) } - private func getImageSynchronous(message: Message, userLocation: MediaResourceUserLocation, media: Media, width: Int, possibleWidths: [Int]) -> GetMediaResult? { + private func getImageSynchronous(message: Message, userLocation: MediaResourceUserLocation, media: Media, width: Int, possibleWidths: [Int], includeBlurred: Bool) -> GetMediaResult? { var immediateThumbnailData: Data? var resource: (resource: MediaResourceReference, size: Int64)? if let image = media as? TelegramMediaImage { @@ -298,39 +344,50 @@ public final class DirectMediaImageCache { guard let resource = resource else { return nil } - + + var resultImage: UIImage? var blurredImage: UIImage? for otherWidth in possibleWidths.reversed() { if otherWidth == width { if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .square(width: otherWidth)))), let image = loadImage(data: data) { - return GetMediaResult(image: image, loadSignal: nil) + if blurredImage == nil, includeBlurred, let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data), let blurredImageValue = generateBlurredThumbnail(image: image, adjustSaturation: true) { + blurredImage = blurredImageValue + } + return GetMediaResult(image: image, blurredImage: blurredImage, loadSignal: nil) } } else { if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .square(width: otherWidth)))), let image = loadImage(data: data) { - blurredImage = image + resultImage = image } } } - if blurredImage == nil { + if resultImage == nil { if let data = try? Data(contentsOf: URL(fileURLWithPath: self.getCachePath(resourceId: resource.resource.resource.id, imageType: .blurredThumbnail))), let image = loadImage(data: data) { - blurredImage = image + resultImage = image } else if let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data) { if let blurredImageValue = generateBlurredThumbnail(image: image) { + resultImage = blurredImageValue + } + if includeBlurred, let blurredImageValue = generateBlurredThumbnail(image: image, adjustSaturation: true) { blurredImage = blurredImageValue } } } + + if blurredImage == nil, includeBlurred, let data = immediateThumbnailData.flatMap(decodeTinyThumbnail), let image = loadImage(data: data), let blurredImageValue = generateBlurredThumbnail(image: image, adjustSaturation: true) { + blurredImage = blurredImageValue + } - return GetMediaResult(image: blurredImage, loadSignal: self.getLoadSignal(width: width, userLocation: userLocation, userContentType: .image, resource: resource.resource, resourceSizeLimit: resource.size)) + return GetMediaResult(image: resultImage, blurredImage: blurredImage, loadSignal: self.getLoadSignal(width: width, userLocation: userLocation, userContentType: .image, resource: resource.resource, resourceSizeLimit: resource.size)) } - public func getImage(message: Message, media: Media, width: Int, possibleWidths: [Int], synchronous: Bool) -> GetMediaResult? { + public func getImage(message: Message, media: Media, width: Int, possibleWidths: [Int], includeBlurred: Bool = false, synchronous: Bool) -> GetMediaResult? { if synchronous { - return self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, possibleWidths: possibleWidths) + return self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, possibleWidths: possibleWidths, includeBlurred: includeBlurred) } else { - return GetMediaResult(image: nil, loadSignal: Signal { subscriber in - let result = self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, possibleWidths: possibleWidths) + return GetMediaResult(image: nil, blurredImage: nil, loadSignal: Signal { subscriber in + let result = self.getImageSynchronous(message: message, userLocation: .peer(message.id.peerId), media: media, width: width, possibleWidths: possibleWidths, includeBlurred: includeBlurred) guard let result = result else { subscriber.putNext(nil) subscriber.putCompletion() diff --git a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift index 38704fc099..3de61b6081 100644 --- a/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/MediaDustNode.swift @@ -6,6 +6,89 @@ import Display import AppBundle import LegacyComponents +public class MediaDustLayer: CALayer { + private var emitter: CAEmitterCell? + private var emitterLayer: CAEmitterLayer? + + private var size: CGSize? + + override public init() { + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupEmitterLayerIfNeeded() { + guard self.emitterLayer == nil else { + return + } + + let emitter = CAEmitterCell() + emitter.color = UIColor(rgb: 0xffffff, alpha: 0.0).cgColor + emitter.contents = UIImage(bundleImageName: "Components/TextSpeckle")?.cgImage + emitter.contentsScale = 1.8 + emitter.emissionRange = .pi * 2.0 + emitter.lifetime = 8.0 + emitter.scale = 0.5 + emitter.velocityRange = 0.0 + emitter.name = "dustCell" + emitter.alphaRange = 1.0 + emitter.setValue("point", forKey: "particleType") + emitter.setValue(1.0, forKey: "mass") + emitter.setValue(0.01, forKey: "massRange") + self.emitter = emitter + + let alphaBehavior = createEmitterBehavior(type: "valueOverLife") + alphaBehavior.setValue("color.alpha", forKey: "keyPath") + alphaBehavior.setValue([0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1, 0, 0, 1, 0, -1], forKey: "values") + alphaBehavior.setValue(true, forKey: "additive") + + let scaleBehavior = createEmitterBehavior(type: "valueOverLife") + scaleBehavior.setValue("scale", forKey: "keyPath") + scaleBehavior.setValue([0.0, 0.5], forKey: "values") + scaleBehavior.setValue([0.0, 0.05], forKey: "locations") + + let behaviors = [alphaBehavior, scaleBehavior] + + let emitterLayer = CAEmitterLayer() + emitterLayer.masksToBounds = true + emitterLayer.allowsGroupOpacity = true + emitterLayer.lifetime = 1 + emitterLayer.emitterCells = [emitter] + emitterLayer.seed = arc4random() + emitterLayer.emitterShape = .rectangle + emitterLayer.setValue(behaviors, forKey: "emitterBehaviors") + self.addSublayer(emitterLayer) + + self.emitterLayer = emitterLayer + } + + private func updateEmitter() { + guard let size = self.size else { + return + } + + self.setupEmitterLayerIfNeeded() + + self.emitterLayer?.frame = CGRect(origin: CGPoint(), size: size) + self.emitterLayer?.emitterSize = size + self.emitterLayer?.emitterPosition = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + + let square = Float(size.width * size.height) + Queue.mainQueue().async { + self.emitter?.birthRate = min(100000.0, square * 0.02) + } + } + + public func updateLayout(size: CGSize) { + self.size = size + + self.updateEmitter() + } +} + public class MediaDustNode: ASDisplayNode { private var currentParams: (size: CGSize, color: UIColor)? private var animColor: CGColor? diff --git a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift index e3874a41a1..a5fff66eab 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/Panes/PeerInfoVisualMediaPaneNode.swift @@ -24,6 +24,7 @@ import TelegramUIPreferences import CheckNode import AppBundle import ChatControllerInteraction +import InvisibleInkDustNode private final class FrameSequenceThumbnailNode: ASDisplayNode { private let context: AccountContext @@ -776,9 +777,11 @@ private protocol ItemLayer: SparseItemGridLayer { var disposable: Disposable? { get set } var hasContents: Bool { get set } + func setSpoilerContents(_ contents: Any?) func updateDuration(duration: Int32?, isMin: Bool, minFactor: CGFloat) func updateSelection(theme: CheckNodeTheme, isSelected: Bool?, animated: Bool) + func updateHasSpoiler(hasSpoiler: Bool) func bind(item: VisualMediaItem) func unbind() @@ -789,6 +792,7 @@ private final class GenericItemLayer: CALayer, ItemLayer { var durationLayer: DurationLayer? var minFactor: CGFloat = 1.0 var selectionLayer: GridMessageSelectionLayer? + var dustLayer: MediaDustLayer? var disposable: Disposable? var hasContents: Bool = false @@ -816,6 +820,12 @@ private final class GenericItemLayer: CALayer, ItemLayer { self.contents = image.cgImage } } + + func setSpoilerContents(_ contents: Any?) { + if let image = contents as? UIImage { + self.dustLayer?.contents = image.cgImage + } + } override func action(forKey event: String) -> CAAction? { return nullAction @@ -873,6 +883,24 @@ private final class GenericItemLayer: CALayer, ItemLayer { } } } + + func updateHasSpoiler(hasSpoiler: Bool) { + if hasSpoiler { + if let _ = self.dustLayer { + } else { + let dustLayer = MediaDustLayer() + self.dustLayer = dustLayer + self.addSublayer(dustLayer) + if !self.bounds.isEmpty { + dustLayer.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + dustLayer.updateLayout(size: self.bounds.size) + } + } + } else if let dustLayer = self.dustLayer { + self.dustLayer = nil + dustLayer.removeFromSuperlayer() + } + } func unbind() { self.item = nil @@ -894,6 +922,7 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL var durationLayer: DurationLayer? var minFactor: CGFloat = 1.0 var selectionLayer: GridMessageSelectionLayer? + var dustLayer: MediaDustLayer? var disposable: Disposable? var hasContents: Bool = false @@ -935,6 +964,12 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL } } } + + func setSpoilerContents(_ contents: Any?) { + if let image = contents as? UIImage { + self.dustLayer?.contents = image.cgImage + } + } func bind(item: VisualMediaItem) { self.item = item @@ -988,6 +1023,24 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL } } } + + func updateHasSpoiler(hasSpoiler: Bool) { + if hasSpoiler { + if let _ = self.dustLayer { + } else { + let dustLayer = MediaDustLayer() + self.dustLayer = dustLayer + self.addSublayer(dustLayer) + if !self.bounds.isEmpty { + dustLayer.frame = CGRect(origin: CGPoint(), size: self.bounds.size) + dustLayer.updateLayout(size: self.bounds.size) + } + } + } else if let dustLayer = self.dustLayer { + self.dustLayer = nil + dustLayer.removeFromSuperlayer() + } + } func unbind() { self.item = nil @@ -1194,6 +1247,8 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme var onBeginFastScrollingImpl: (() -> Void)? var getShimmerColorsImpl: (() -> SparseItemGrid.ShimmerColors)? var updateShimmerLayersImpl: ((SparseItemGridDisplayItem) -> Void)? + + var revealedSpoilerMessageIds = Set() private var shimmerImages: [CGFloat: UIImage] = [:] @@ -1395,7 +1450,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme } let message = item.message - + let hasSpoiler = message.attributes.contains(where: { $0 is MediaSpoilerMessageAttribute }) && !self.revealedSpoilerMessageIds.contains(message.id) + layer.updateHasSpoiler(hasSpoiler: hasSpoiler) + var selectedMedia: Media? for media in message.media { if let image = media as? TelegramMediaImage { @@ -1408,7 +1465,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme } if let selectedMedia = selectedMedia { - if let result = directMediaImageCache.getImage(message: message, media: selectedMedia, width: imageWidthSpec, possibleWidths: SparseItemGridBindingImpl.widthSpecs.1, synchronous: synchronous == .full) { + if let result = directMediaImageCache.getImage(message: message, media: selectedMedia, width: imageWidthSpec, possibleWidths: SparseItemGridBindingImpl.widthSpecs.1, includeBlurred: hasSpoiler, synchronous: synchronous == .full) { if let image = result.image { layer.setContents(image) switch synchronous { @@ -1423,6 +1480,9 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme layer.hasContents = true } } + if let image = result.blurredImage { + layer.setSpoilerContents(image) + } if let loadSignal = result.loadSignal { layer.disposable?.dispose() let startTimestamp = CFAbsoluteTimeGetCurrent() @@ -1494,7 +1554,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding, ListShimme } else { layer.updateSelection(theme: self.checkNodeTheme, isSelected: nil, animated: false) } - + layer.bind(item: item) } } @@ -1664,7 +1724,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - + init(context: AccountContext, chatControllerInteraction: ChatControllerInteraction, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, contentType: ContentType, captureProtected: Bool) { self.context = context self.peerId = peerId @@ -2316,6 +2376,8 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro if let item = itemLayer.item { if self.itemInteraction.hiddenMedia[item.message.id] != nil { itemLayer.isHidden = true + itemLayer.updateHasSpoiler(hasSpoiler: false) + self.itemGridBinding.revealedSpoilerMessageIds.insert(item.message.id) } else { itemLayer.isHidden = false }