import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox import TelegramCore import ComponentFlow import TelegramPresentationData import PhotoResources import AvatarNode import AccountContext import InvisibleInkDustNode final class StarsParticlesView: UIView { private struct Particle { var trackIndex: Int var position: CGPoint var scale: CGFloat var alpha: CGFloat var direction: CGPoint var velocity: CGFloat var color: UIColor var currentTime: CGFloat var lifeTime: CGFloat init( trackIndex: Int, position: CGPoint, scale: CGFloat, alpha: CGFloat, direction: CGPoint, velocity: CGFloat, color: UIColor, currentTime: CGFloat, lifeTime: CGFloat ) { self.trackIndex = trackIndex self.position = position self.scale = scale self.alpha = alpha self.direction = direction self.velocity = velocity self.color = color self.currentTime = currentTime self.lifeTime = lifeTime } mutating func update(deltaTime: CGFloat) { var position = self.position position.x += self.direction.x * self.velocity * deltaTime position.y += self.direction.y * self.velocity * deltaTime self.position = position self.currentTime += deltaTime } } private final class ParticleSet { private let size: CGSize private let large: Bool private(set) var particles: [Particle] = [] init(size: CGSize, large: Bool, preAdvance: Bool) { self.size = size self.large = large self.generateParticles(preAdvance: preAdvance) } private func generateParticles(preAdvance: Bool) { let maxDirections = self.large ? 8 : 80 if self.particles.count < maxDirections { var allTrackIndices: [Int] = Array(repeating: 0, count: maxDirections) for i in 0 ..< maxDirections { allTrackIndices[i] = i } var takenIndexCount = 0 for particle in self.particles { allTrackIndices[particle.trackIndex] = -1 takenIndexCount += 1 } var availableTrackIndices: [Int] = [] availableTrackIndices.reserveCapacity(maxDirections - takenIndexCount) for index in allTrackIndices { if index != -1 { availableTrackIndices.append(index) } } if !availableTrackIndices.isEmpty { availableTrackIndices.shuffle() for takeIndex in availableTrackIndices { let directionIndex = takeIndex var angle = (CGFloat(directionIndex % maxDirections) / CGFloat(maxDirections)) * CGFloat.pi * 2.0 var alpha = 1.0 var lifeTimeMultiplier = 1.0 var isUpOrDownSemisphere = false if angle > CGFloat.pi / 7.0 && angle < CGFloat.pi - CGFloat.pi / 7.0 { isUpOrDownSemisphere = true } else if !"".isEmpty, angle > CGFloat.pi + CGFloat.pi / 7.0 && angle < 2.0 * CGFloat.pi - CGFloat.pi / 7.0 { isUpOrDownSemisphere = true } if isUpOrDownSemisphere { if CGFloat.random(in: 0.0 ... 1.0) < 0.2 { lifeTimeMultiplier = 0.3 } else { angle += CGFloat.random(in: 0.0 ... 1.0) > 0.5 ? CGFloat.pi / 1.6 : -CGFloat.pi / 1.6 angle += CGFloat.random(in: -0.2 ... 0.2) lifeTimeMultiplier = 0.5 } if self.large { alpha = 0.0 } } if self.large { angle += CGFloat.random(in: -0.5 ... 0.5) } let direction = CGPoint(x: cos(angle), y: sin(angle)) let velocity = self.large ? CGFloat.random(in: 15.0 ..< 20.0) : CGFloat.random(in: 20.0 ..< 35.0) let scale = self.large ? CGFloat.random(in: 0.65 ... 0.9) : CGFloat.random(in: 0.65 ... 1.0) * 0.75 let lifeTime = (self.large ? CGFloat.random(in: 2.0 ... 3.5) : CGFloat.random(in: 0.7 ... 3.0)) var position = CGPoint(x: self.size.width / 2.0, y: self.size.height / 2.0) var initialOffset: CGFloat = 0.5 if preAdvance { initialOffset = CGFloat.random(in: 0.5 ... 1.0) } else { let p = CGFloat.random(in: 0.0 ... 1.0) if p < 0.5 { initialOffset = CGFloat.random(in: 0.65 ... 1.0) } else { initialOffset = 0.5 } } position.x += direction.x * initialOffset * 105.0 position.y += direction.y * initialOffset * 105.0 let largeColors: [UInt32] = [0xff9145, 0xfec007, 0xed9303] let smallColors: [UInt32] = [0xfecc14, 0xf7ab04, 0xff9145, 0xfdda21] let particle = Particle( trackIndex: directionIndex, position: position, scale: scale, alpha: alpha, direction: direction, velocity: velocity, color: UIColor(rgb: (self.large ? largeColors : smallColors).randomElement()!), currentTime: 0.0, lifeTime: lifeTime * lifeTimeMultiplier ) self.particles.append(particle) } } } } func update(deltaTime: CGFloat) { for i in (0 ..< self.particles.count).reversed() { self.particles[i].update(deltaTime: deltaTime) if self.particles[i].currentTime > self.particles[i].lifeTime { self.particles.remove(at: i) } } self.generateParticles(preAdvance: false) } } private var displayLink: SharedDisplayLinkDriver.Link? private var particleSet: ParticleSet? private let particleImage: UIImage private var particleLayers: [SimpleLayer] = [] private var size: CGSize? private let large: Bool init(size: CGSize, large: Bool) { if large { self.particleImage = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/PremiumIcon"), color: .white)!.withRenderingMode(.alwaysTemplate) } else { self.particleImage = generateTintedImage(image: UIImage(bundleImageName: "Premium/Stars/Particle"), color: .white)!.withRenderingMode(.alwaysTemplate) } self.large = large super.init(frame: .zero) self.particleSet = ParticleSet(size: size, large: large, preAdvance: true) self.displayLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] delta in self?.update(deltaTime: CGFloat(delta)) }) } required init?(coder: NSCoder) { preconditionFailure() } fileprivate func update(size: CGSize) { self.size = size } private func update(deltaTime: CGFloat) { guard let particleSet = self.particleSet else { return } particleSet.update(deltaTime: deltaTime) for i in 0 ..< particleSet.particles.count { let particle = particleSet.particles[i] let particleLayer: SimpleLayer if i < self.particleLayers.count { particleLayer = self.particleLayers[i] particleLayer.isHidden = false } else { particleLayer = SimpleLayer() particleLayer.contents = self.particleImage.cgImage particleLayer.bounds = CGRect(origin: CGPoint(), size: particleImage.size) self.particleLayers.append(particleLayer) self.layer.addSublayer(particleLayer) } particleLayer.layerTintColor = particle.color.cgColor particleLayer.position = particle.position particleLayer.opacity = Float(particle.alpha) let particleScale = min(1.0, particle.currentTime / 0.3) * min(1.0, (particle.lifeTime - particle.currentTime) / 0.2) * particle.scale particleLayer.transform = CATransform3DMakeScale(particleScale, particleScale, 1.0) } if particleSet.particles.count < self.particleLayers.count { for i in particleSet.particles.count ..< self.particleLayers.count { self.particleLayers[i].isHidden = true } } } } public final class StarsImageComponent: Component { public enum Subject: Equatable { case none case photo(TelegramMediaWebFile) case media([Media]) case extendedMedia([TelegramExtendedMedia]) case transactionPeer(StarsContext.State.Transaction.Peer) public static func == (lhs: StarsImageComponent.Subject, rhs: StarsImageComponent.Subject) -> Bool { switch lhs { case .none: if case .none = rhs { return true } else { return false } case let .photo(lhsPhoto): if case let .photo(rhsPhoto) = rhs, lhsPhoto == rhsPhoto { return true } else { return false } case let .media(lhsMedia): if case let .media(rhsMedia) = rhs, areMediaArraysEqual(lhsMedia, rhsMedia) { return true } else { return false } case let .extendedMedia(lhsExtendedMedia): if case let .extendedMedia(rhsExtendedMedia) = rhs, lhsExtendedMedia == rhsExtendedMedia { return true } else { return false } case let .transactionPeer(lhsPeer): if case let .transactionPeer(rhsPeer) = rhs, lhsPeer == rhsPeer { return true } else { return false } } } } public let context: AccountContext public let subject: Subject public let theme: PresentationTheme public let diameter: CGFloat public let backgroundColor: UIColor public let action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? public init( context: AccountContext, subject: Subject, theme: PresentationTheme, diameter: CGFloat, backgroundColor: UIColor, action: ((@escaping (Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?, @escaping (UIView) -> Void) -> Void)? = nil ) { self.context = context self.subject = subject self.theme = theme self.diameter = diameter self.backgroundColor = backgroundColor self.action = action } public static func ==(lhs: StarsImageComponent, rhs: StarsImageComponent) -> Bool { if lhs.context !== rhs.context { return false } if lhs.subject != rhs.subject { return false } if lhs.theme !== rhs.theme { return false } if lhs.diameter != rhs.diameter { return false } if lhs.backgroundColor != rhs.backgroundColor { return false } return true } public final class View: UIView { private var component: StarsImageComponent? private var state: EmptyComponentState? private var smallParticlesView: StarsParticlesView? private var largeParticlesView: StarsParticlesView? private var containerNode: ASDisplayNode? private var imageNode: TransformImageNode? private var imageFrameNode: UIView? private var secondImageNode: TransformImageNode? private var avatarNode: ImageNode? private var iconBackgroundView: UIImageView? private var iconView: UIImageView? private var dustNode: MediaDustNode? private var button: UIControl? private var countView = ComponentView() private let fetchDisposable = MetaDisposable() private var hiddenMediaDisposable: Disposable? private var hiddenMedia: [Media] = [] public override init(frame: CGRect) { super.init(frame: frame) } required init?(coder: NSCoder) { preconditionFailure() } deinit { self.fetchDisposable.dispose() self.hiddenMediaDisposable?.dispose() } @objc private func buttonPressed() { guard let component = self.component else { return } component.action?({ [weak self] media in guard let self else { return nil } return self.transitionNode(media) }, { [weak self] view in guard let self else { return } self.superview?.addSubview(view) }) } public func transitionNode(_ transitionMedia: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { guard let component = self.component, let containerNode = self.containerNode else { return nil } if case let .media(media) = component.subject, media.first?.id == transitionMedia.id { return (containerNode, containerNode.bounds, { [weak containerNode] in return (containerNode?.view.snapshotContentTree(unhide: true), nil) }) } return nil } func update(component: StarsImageComponent, state: EmptyComponentState, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component self.state = state let smallParticlesView: StarsParticlesView if let current = self.smallParticlesView { smallParticlesView = current } else { smallParticlesView = StarsParticlesView(size: availableSize, large: false) self.addSubview(smallParticlesView) self.smallParticlesView = smallParticlesView } smallParticlesView.update(size: availableSize) smallParticlesView.frame = CGRect(origin: .zero, size: availableSize) let largeParticlesView: StarsParticlesView if let current = self.largeParticlesView { largeParticlesView = current } else { largeParticlesView = StarsParticlesView(size: availableSize, large: true) self.addSubview(largeParticlesView) self.largeParticlesView = largeParticlesView } largeParticlesView.update(size: availableSize) largeParticlesView.frame = CGRect(origin: .zero, size: availableSize) let containerNode: ASDisplayNode if let current = self.containerNode { containerNode = current } else { containerNode = ASDisplayNode() self.addSubview(containerNode.view) self.containerNode = containerNode } var imageSize = CGSize(width: component.diameter, height: component.diameter) let containerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - imageSize.width) / 2.0), y: floorToScreenPixels((availableSize.height - imageSize.height) / 2.0)), size: imageSize) containerNode.frame = containerFrame if case let .media(media) = component.subject, media.count > 1 { imageSize = CGSize(width: component.diameter - 6.0, height: component.diameter - 6.0) } else if case let .extendedMedia(media) = component.subject, media.count > 1 { imageSize = CGSize(width: component.diameter - 6.0, height: component.diameter - 6.0) } let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((containerFrame.width - imageSize.width) / 2.0), y: floorToScreenPixels((containerFrame.height - imageSize.height) / 2.0)), size: imageSize) switch component.subject { case .none: break case let .photo(photo): let imageNode: TransformImageNode if let current = self.imageNode { imageNode = current } else { imageNode = TransformImageNode() imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] containerNode.view.addSubview(imageNode.view) self.imageNode = imageNode imageNode.setSignal(chatWebFileImage(account: component.context.account, file: photo)) self.fetchDisposable.set(chatMessageWebFileInteractiveFetched(account: component.context.account, userLocation: .other, image: photo).startStrict()) } imageNode.frame = imageFrame imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: imageSize.width / 2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() case let .media(media): let imageNode: TransformImageNode var dimensions = imageSize var isFirstTime = false if let current = self.imageNode { imageNode = current } else { isFirstTime = true imageNode = TransformImageNode() imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] containerNode.view.addSubview(imageNode.view) self.imageNode = imageNode } if let image = media.first as? TelegramMediaImage { if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions { dimensions = imageDimensions.cgSize.aspectFilled(imageSize) } if isFirstTime { imageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false)) } } else if let file = media.first as? TelegramMediaFile { if let videoDimensions = file.dimensions { dimensions = videoDimensions.cgSize.aspectFilled(imageSize) } if isFirstTime { imageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true)) } } imageNode.frame = imageFrame imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: dimensions, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() if let firstMedia = media.first, self.hiddenMedia.contains(where: { $0.id == firstMedia.id }) { containerNode.isHidden = true } else { containerNode.isHidden = false } if media.count > 1 { let secondImageNode: TransformImageNode let imageFrameNode: UIView var secondDimensions = imageSize if let current = self.secondImageNode, let currentFrame = self.imageFrameNode { secondImageNode = current imageFrameNode = currentFrame } else { secondImageNode = TransformImageNode() secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] containerNode.view.insertSubview(secondImageNode.view, belowSubview: imageNode.view) self.secondImageNode = secondImageNode imageFrameNode = UIView() imageFrameNode.layer.cornerRadius = 17.0 containerNode.view.insertSubview(imageFrameNode, belowSubview: imageNode.view) self.imageFrameNode = imageFrameNode } if let image = media[1] as? TelegramMediaImage { if let imageDimensions = largestImageRepresentation(image.representations)?.dimensions { secondDimensions = imageDimensions.cgSize.aspectFilled(imageSize) } if isFirstTime { secondImageNode.setSignal(chatMessagePhotoThumbnail(account: component.context.account, userLocation: .other, photoReference: .standalone(media: image), onlyFullSize: false, blurred: false)) } } else if let file = media[1] as? TelegramMediaFile { if let videoDimensions = file.dimensions { secondDimensions = videoDimensions.cgSize.aspectFilled(imageSize) } if isFirstTime { secondImageNode.setSignal(mediaGridMessageVideo(postbox: component.context.account.postbox, userLocation: .other, videoReference: .standalone(media: file), useLargeThumbnail: true, autoFetchFullSizeThumbnail: true)) } } imageFrameNode.backgroundColor = component.backgroundColor secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: secondDimensions, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() secondImageNode.frame = imageFrame.offsetBy(dx: 6.0, dy: -6.0) imageFrameNode.frame = imageFrame.insetBy(dx: -2.0, dy: -2.0) let countSize = self.countView.update( transition: .immediate, component: AnyComponent( Text(text: "\(media.count)", font: Font.with(size: 30.0, design: .round, weight: .medium), color: .white) ), environment: {}, containerSize: imageFrame.size ) let countFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - countSize.width) / 2.0), y: imageFrame.minY + floorToScreenPixels((imageFrame.height - countSize.height) / 2.0)), size: countSize) if let countView = self.countView.view { if countView.superview == nil { containerNode.view.addSubview(countView) } countView.frame = countFrame } } case let .extendedMedia(extendedMedia): let imageNode: TransformImageNode let dustNode: MediaDustNode var dimensions = imageSize var isFirstTime = false if let current = self.imageNode, let currentDust = self.dustNode { imageNode = current dustNode = currentDust } else { isFirstTime = true imageNode = TransformImageNode() imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] containerNode.view.addSubview(imageNode.view) self.imageNode = imageNode dustNode = MediaDustNode(enableAnimations: true) dustNode.isUserInteractionEnabled = false containerNode.view.addSubview(dustNode.view) self.dustNode = dustNode } let media: TelegramMediaImage switch extendedMedia.first { case let .preview(imageDimensions, immediateThumbnailData, _): let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) media = thumbnailMedia if let imageDimensions { dimensions = imageDimensions.cgSize.aspectFilled(imageSize) } default: fatalError() } if isFirstTime { imageNode.setSignal(chatSecretPhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: media), ignoreFullSize: true, synchronousLoad: true)) } imageNode.frame = imageFrame imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: dimensions, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() dustNode.frame = imageFrame dustNode.update(size: imageFrame.size, color: .white, transition: .immediate) if extendedMedia.count > 1 { let secondImageNode: TransformImageNode let imageFrameNode: UIView var secondDimensions = imageSize var isFirstTime = false if let current = self.secondImageNode, let currentFrame = self.imageFrameNode { secondImageNode = current imageFrameNode = currentFrame } else { isFirstTime = true secondImageNode = TransformImageNode() secondImageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] containerNode.view.insertSubview(secondImageNode.view, belowSubview: imageNode.view) self.secondImageNode = secondImageNode imageFrameNode = UIView() imageFrameNode.layer.cornerRadius = 17.0 containerNode.view.insertSubview(imageFrameNode, belowSubview: imageNode.view) self.imageFrameNode = imageFrameNode } let media: TelegramMediaImage switch extendedMedia[1] { case let .preview(imageDimensions, immediateThumbnailData, _): let thumbnailMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [], immediateThumbnailData: immediateThumbnailData, reference: nil, partialReference: nil, flags: []) media = thumbnailMedia if let imageDimensions { secondDimensions = imageDimensions.cgSize.aspectFilled(imageSize) } default: fatalError() } if isFirstTime { secondImageNode.setSignal(chatSecretPhoto(account: component.context.account, userLocation: .other, photoReference: .standalone(media: media), ignoreFullSize: true, synchronousLoad: true)) } imageFrameNode.backgroundColor = component.backgroundColor secondImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(radius: 16.0), imageSize: secondDimensions, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: component.theme.list.mediaPlaceholderColor))() secondImageNode.frame = imageFrame.offsetBy(dx: 6.0, dy: -6.0) imageFrameNode.frame = imageFrame.insetBy(dx: -2.0, dy: -2.0) } if extendedMedia.count > 1 { let countSize = self.countView.update( transition: .immediate, component: AnyComponent( Text(text: "\(extendedMedia.count)", font: Font.with(size: 30.0, design: .round, weight: .medium), color: .white) ), environment: {}, containerSize: imageFrame.size ) let countFrame = CGRect(origin: CGPoint(x: imageFrame.minX + floorToScreenPixels((imageFrame.width - countSize.width) / 2.0), y: imageFrame.minY + floorToScreenPixels((imageFrame.height - countSize.height) / 2.0)), size: countSize) if let countView = self.countView.view { if countView.superview == nil { containerNode.view.addSubview(countView) } countView.frame = countFrame } } case let .transactionPeer(peer): if case let .peer(peer) = peer { let avatarNode: ImageNode if let current = self.avatarNode { avatarNode = current } else { avatarNode = ImageNode() avatarNode.displaysAsynchronously = false containerNode.view.addSubview(avatarNode.view) self.avatarNode = avatarNode avatarNode.setSignal(peerAvatarCompleteImage(account: component.context.account, peer: peer, size: imageSize, font: avatarPlaceholderFont(size: 43.0), fullSize: true)) } avatarNode.frame = imageFrame } else { let iconBackgroundView: UIImageView let iconView: UIImageView if let currentBackground = self.iconBackgroundView, let current = self.iconView { iconBackgroundView = currentBackground iconView = current } else { iconBackgroundView = UIImageView() iconView = UIImageView() containerNode.view.addSubview(iconBackgroundView) containerNode.view.addSubview(iconView) self.iconBackgroundView = iconBackgroundView self.iconView = iconView } var iconInset: CGFloat = 9.0 var iconOffset: CGFloat = 0.0 switch peer { case .appStore: iconBackgroundView.image = generateGradientFilledCircleImage( diameter: imageSize.width, colors: [ UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor ], direction: .mirroredDiagonal ) iconView.image = UIImage(bundleImageName: "Premium/Stars/Apple") case .playMarket: iconBackgroundView.image = generateGradientFilledCircleImage( diameter: imageSize.width, colors: [ UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor ], direction: .mirroredDiagonal ) iconView.image = UIImage(bundleImageName: "Premium/Stars/Google") case .fragment: iconBackgroundView.image = generateFilledCircleImage( diameter: imageSize.width, color: UIColor(rgb: 0x1b1f24) ) iconView.image = UIImage(bundleImageName: "Premium/Stars/Fragment") iconOffset = 5.0 case .ads: iconBackgroundView.image = generateGradientFilledCircleImage( diameter: imageSize.width, colors: [ UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor ], direction: .mirroredDiagonal ) iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white) case .premiumBot: iconInset = 15.0 iconBackgroundView.image = generateGradientFilledCircleImage( diameter: imageSize.width, colors: [ UIColor(rgb: 0x6b93ff).cgColor, UIColor(rgb: 0x6b93ff).cgColor, UIColor(rgb: 0x8d77ff).cgColor, UIColor(rgb: 0xb56eec).cgColor, UIColor(rgb: 0xb56eec).cgColor ], direction: .mirroredDiagonal ) iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) case .peer, .unsupported: iconInset = 15.0 iconBackgroundView.image = generateGradientFilledCircleImage( diameter: imageSize.width, colors: [ UIColor(rgb: 0xb1b1b1).cgColor, UIColor(rgb: 0xcdcdcd).cgColor ], direction: .mirroredDiagonal ) iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/EntityInputPremiumIcon"), color: .white) } iconBackgroundView.frame = imageFrame iconView.frame = imageFrame.insetBy(dx: iconInset, dy: iconInset).offsetBy(dx: 0.0, dy: iconOffset) } } if let _ = component.action { if self.button == nil { let button = UIControl(frame: imageFrame) button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) containerNode.view.addSubview(button) self.button = button } } else if let button = self.button { self.button = nil button.removeFromSuperview() } if case .media = component.subject { if self.hiddenMediaDisposable == nil { self.hiddenMediaDisposable = component.context.sharedContext.mediaManager.galleryHiddenMediaManager.hiddenIds().startStrict(next: { [weak self] ids in guard let self, let component = self.component else { return } var hiddenMedia: [Media] = [] for id in ids { if case let .chat(accountId, _, media) = id, accountId == component.context.account.id { hiddenMedia.append(media) } } self.hiddenMedia = hiddenMedia self.state?.updated() }).strict() } } else if let hiddenMediaDisposable = self.hiddenMediaDisposable { self.hiddenMediaDisposable = nil hiddenMediaDisposable.dispose() } return availableSize } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, state: state, availableSize: availableSize, transition: transition) } }