diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index c74db47d29..e19e7f694a 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -553,18 +553,18 @@ public extension ContainedViewLayoutTransition { } } - func animateFrame(layer: CALayer, from frame: CGRect, to toFrame: CGRect? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + func animateFrame(layer: CALayer, from frame: CGRect, to toFrame: CGRect? = nil, delay: Double = 0.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self { - case .immediate: + case .immediate: + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + layer.animateFrame(from: frame, to: toFrame ?? layer.frame, duration: duration, delay: delay, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in if let completion = completion { - completion(true) + completion(result) } - case let .animated(duration, curve): - layer.animateFrame(from: frame, to: toFrame ?? layer.frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in - if let completion = completion { - completion(result) - } - }) + }) } } diff --git a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift index d4dfa2809e..f522d8bb2d 100644 --- a/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift +++ b/submodules/InvisibleInkDustNode/Sources/InvisibleInkDustNode.swift @@ -591,6 +591,29 @@ public class InvisibleInkDustNode: ASDisplayNode { } } + public func revealWithoutMaskAtLocation(_ location: CGPoint) { + guard !self.isRevealed else { + return + } + + self.isRevealed = true + + if self.enableAnimations { + self.isExploding = true + + self.emitterLayer?.setValue(true, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") + self.emitterLayer?.setValue(location, forKeyPath: "emitterBehaviors.fingerAttractor.position") + + Queue.mainQueue().after(0.8 * UIView.animationDurationFactor()) { + self.isExploding = false + self.emitterLayer?.setValue(false, forKeyPath: "emitterBehaviors.fingerAttractor.enabled") + } + } else { + self.staticNode?.alpha = 0.0 + self.staticNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + } + } + @objc private func tap(_ gestureRecognizer: UITapGestureRecognizer) { let location = gestureRecognizer.location(in: self.view) self.revealAtLocation(location) diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 2f6ed085d3..b987d51f8e 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -683,7 +683,22 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var spoilerExpandRect: CGRect? if let location = strongSelf.displayContentsUnderSpoilers.location { - spoilerExpandRect = textFrame.size.centered(around: CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY)) + strongSelf.displayContentsUnderSpoilers.location = nil + + let mappedLocation = CGPoint(x: location.x - textFrame.minX, y: location.y - textFrame.minY) + + let getDistance: (CGPoint, CGPoint) -> CGFloat = { a, b in + let v = CGPoint(x: a.x - b.x, y: a.y - b.y) + return sqrt(v.x * v.x + v.y * v.y) + } + + var maxDistance: CGFloat = getDistance(mappedLocation, CGPoint(x: 0.0, y: 0.0)) + maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: textFrame.width, y: 0.0))) + maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: textFrame.width, y: textFrame.height))) + maxDistance = max(maxDistance, getDistance(mappedLocation, CGPoint(x: 0.0, y: textFrame.height))) + + let mappedSize = CGSize(width: maxDistance * 2.0, height: maxDistance * 2.0) + spoilerExpandRect = mappedSize.centered(around: mappedLocation) } let _ = textApply(InteractiveTextNodeWithEntities.Arguments( @@ -694,8 +709,11 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { attemptSynchronous: synchronousLoads, textColor: messageTheme.primaryTextColor, spoilerEffectColor: messageTheme.secondaryTextColor, - animation: animation, - animationArguments: InteractiveTextNode.AnimationArguments( + applyArguments: InteractiveTextNode.ApplyArguments( + animation: animation, + spoilerTextColor: messageTheme.primaryTextColor, + spoilerEffectColor: messageTheme.secondaryTextColor, + areContentAnimationsEnabled: item.context.sharedContext.energyUsageSettings.loopEmoji, spoilerExpandRect: spoilerExpandRect ) )) @@ -1398,7 +1416,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return } self.displayContentsUnderSpoilers = (value, location) - self.displayContentsUnderSpoilers.location = nil + //self.displayContentsUnderSpoilers.location = nil if let item = self.item { item.controllerInteraction.requestMessageUpdate(item.message.id, false) } diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 6613b4752b..18f78c5b22 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -352,6 +352,44 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } } + public weak var mirrorLayer: CALayer? { + didSet { + if let mirrorLayer = self.mirrorLayer { + mirrorLayer.contents = self.contents + + var customColor = self.contentTintColor + if let file = self.file { + if file.isCustomTemplateEmoji { + customColor = self.dynamicColor + } + } + + if customColor != nil { + if self.layerTintColor == nil { + setLayerContentsMaskMode(mirrorLayer, true) + } + } else { + if self.layerTintColor != nil { + setLayerContentsMaskMode(mirrorLayer, false) + } + } + if let customColor { + Transition.immediate.setTintColor(layer: mirrorLayer, color: customColor) + } else { + self.layerTintColor = nil + } + } + } + } + + override public var contents: Any? { + didSet { + if let mirrorLayer = self.mirrorLayer { + mirrorLayer.contents = self.contents + } + } + } + public convenience init(context: AccountContext, userLocation: MediaResourceUserLocation, attemptSynchronousLoad: Bool, emoji: ChatTextInputTextCustomEmojiAttribute, file: TelegramMediaFile?, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool = false, placeholderColor: UIColor, pointSize: CGSize, dynamicColor: UIColor? = nil, loopCount: Int? = nil) { self.init( context: .account(context), diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift index bfd9029500..cf280a74d0 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextComponent.swift @@ -7,6 +7,8 @@ import AppBundle import ComponentFlow import TextFormat import MessageInlineBlockBackgroundView +import InvisibleInkDustNode +import EmojiTextAttachmentView private let defaultFont = UIFont.systemFont(ofSize: 15.0) @@ -1073,10 +1075,24 @@ private func addAttachment(attachment: UIImage, line: InteractiveTextNodeLine, a } open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecognizerDelegate { - public final class AnimationArguments { + public final class ApplyArguments { + public let animation: ListViewItemUpdateAnimation + public let spoilerTextColor: UIColor + public let spoilerEffectColor: UIColor + public let areContentAnimationsEnabled: Bool public let spoilerExpandRect: CGRect? - public init(spoilerExpandRect: CGRect?) { + public init( + animation: ListViewItemUpdateAnimation, + spoilerTextColor: UIColor, + spoilerEffectColor: UIColor, + areContentAnimationsEnabled: Bool, + spoilerExpandRect: CGRect? + ) { + self.animation = animation + self.spoilerTextColor = spoilerTextColor + self.spoilerEffectColor = spoilerEffectColor + self.areContentAnimationsEnabled = areContentAnimationsEnabled self.spoilerExpandRect = spoilerExpandRect } } @@ -1154,10 +1170,10 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn continue } - guard let item = contentItemLayer.item else { + guard let params = contentItemLayer.params else { continue } - guard let blockQuote = item.segment.blockQuote else { + guard let blockQuote = params.item.segment.blockQuote else { continue } if blockQuote.isCollapsed == nil { @@ -1684,12 +1700,12 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn return calculateLayoutV2(attributedString: attributedString, minimumNumberOfLines: minimumNumberOfLines, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, backgroundColor: backgroundColor, constrainedSize: constrainedSize, alignment: alignment, verticalAlignment: verticalAlignment, lineSpacingFactor: lineSpacingFactor, cutout: cutout, insets: insets, lineColor: lineColor, textShadowColor: textShadowColor, textShadowBlur: textShadowBlur, textStroke: textStroke, displayContentsUnderSpoilers: displayContentsUnderSpoilers, customTruncationToken: customTruncationToken, expandedBlocks: expandedBlocks) } - private func updateContentItems(animation: ListViewItemUpdateAnimation, animationArguments: AnimationArguments?) { + private func updateContentItems(arguments: ApplyArguments) { guard let cachedLayout = self.cachedLayout else { return } - let animateContents = self.isDisplayingContentsUnderSpoilers != nil && self.isDisplayingContentsUnderSpoilers != cachedLayout.displayContentsUnderSpoilers && animation.isAnimated + let animateContents = self.isDisplayingContentsUnderSpoilers != nil && self.isDisplayingContentsUnderSpoilers != cachedLayout.displayContentsUnderSpoilers && arguments.animation.isAnimated let synchronous = animateContents self.isDisplayingContentsUnderSpoilers = cachedLayout.displayContentsUnderSpoilers @@ -1735,14 +1751,14 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn let contentItemFrame = CGRect(origin: CGPoint(x: segmentRect.minX, y: segmentRect.minY), size: CGSize(width: contentItem.size.width, height: contentItem.size.height)) - var contentItemAnimation = animation + var contentItemAnimation = arguments.animation let contentItemLayer: TextContentItemLayer var itemSpoilerExpandRect: CGRect? var itemAnimateContents = animateContents && contentItemAnimation.isAnimated if let current = self.contentItemLayers[itemId] { contentItemLayer = current - if animation.isAnimated, let spoilerExpandRect = animationArguments?.spoilerExpandRect { + if arguments.animation.isAnimated, let spoilerExpandRect = arguments.spoilerExpandRect { itemSpoilerExpandRect = spoilerExpandRect.offsetBy(dx: -contentItemFrame.minX, dy: -contentItemFrame.minY) itemAnimateContents = true } @@ -1754,7 +1770,12 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn } contentItemLayer.update( - item: contentItem, + params: TextContentItemLayer.Params( + item: contentItem, + spoilerTextColor: arguments.spoilerTextColor, + spoilerEffectColor: arguments.spoilerEffectColor, + areContentAnimationsEnabled: arguments.areContentAnimationsEnabled + ), animation: contentItemAnimation, synchronously: synchronous, animateContents: itemAnimateContents, @@ -1810,7 +1831,7 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn } } - public static func asyncLayout(_ maybeNode: InteractiveTextNode?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (ListViewItemUpdateAnimation, AnimationArguments?) -> InteractiveTextNode) { + public static func asyncLayout(_ maybeNode: InteractiveTextNode?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (ApplyArguments) -> InteractiveTextNode) { let existingLayout: InteractiveTextNodeLayout? = maybeNode?.cachedLayout return { arguments in @@ -1852,10 +1873,10 @@ open class InteractiveTextNode: ASDisplayNode, TextNodeProtocol, UIGestureRecogn let node = maybeNode ?? InteractiveTextNode() - return (layout, { animation, animationArguments in + return (layout, { arguments in if node.cachedLayout !== layout { node.cachedLayout = layout - node.updateContentItems(animation: animation, animationArguments: animationArguments) + node.updateContentItems(arguments: arguments) } return node @@ -1899,6 +1920,25 @@ final class TextContentItem { } final class TextContentItemLayer: SimpleLayer { + final class Params { + let item: TextContentItem + let spoilerTextColor: UIColor + let spoilerEffectColor: UIColor + let areContentAnimationsEnabled: Bool + + init( + item: TextContentItem, + spoilerTextColor: UIColor, + spoilerEffectColor: UIColor, + areContentAnimationsEnabled: Bool + ) { + self.item = item + self.spoilerTextColor = spoilerTextColor + self.spoilerEffectColor = spoilerEffectColor + self.areContentAnimationsEnabled = areContentAnimationsEnabled + } + } + final class RenderMask { let image: UIImage let isOpaque: Bool @@ -2193,10 +2233,15 @@ final class TextContentItemLayer: SimpleLayer { } } - private(set) var item: TextContentItem? + private(set) var params: Params? let renderNode: RenderNode private var contentMaskNode: ASImageNode? + + private var overlayContentLayer: SimpleLayer? + private var overlayContentMaskNode: ASImageNode? + private var spoilerEffectNode: InvisibleInkDustNode? + private var blockBackgroundView: MessageInlineBlockBackgroundView? private var quoteTypeIconNode: ASImageNode? private var blockExpandArrow: SimpleLayer? @@ -2224,19 +2269,19 @@ final class TextContentItemLayer: SimpleLayer { } func update( - item: TextContentItem, + params: Params, animation: ListViewItemUpdateAnimation, synchronously: Bool, animateContents: Bool, spoilerExpandRect: CGRect? ) { - self.item = item + self.params = params - let contentFrame = CGRect(origin: CGPoint(), size: item.size) + let contentFrame = CGRect(origin: CGPoint(), size: params.item.size) var effectiveContentFrame = contentFrame var contentMask: RenderMask? - if let blockQuote = item.segment.blockQuote { + if let blockQuote = params.item.segment.blockQuote { let blockBackgroundView: MessageInlineBlockBackgroundView if let current = self.blockBackgroundView { blockBackgroundView = current @@ -2257,19 +2302,19 @@ final class TextContentItemLayer: SimpleLayer { } blockExpandArrow.layerTintColor = blockQuote.tintColor.cgColor - let blockBackgroundFrame = blockQuote.frame.offsetBy(dx: item.contentOffset.x, dy: item.contentOffset.y + 4.0) + let blockBackgroundFrame = blockQuote.frame.offsetBy(dx: params.item.contentOffset.x, dy: params.item.contentOffset.y + 4.0) if animation.isAnimated { self.isAnimating = true self.currentAnimationId += 1 let animationId = self.currentAnimationId animation.animator.updateFrame(layer: blockBackgroundView.layer, frame: blockBackgroundFrame, completion: { [weak self] completed in - guard completed, let self, self.currentAnimationId == animationId, let item = self.item else { + guard completed, let self, self.currentAnimationId == animationId, let params = self.params else { return } self.isAnimating = false self.update( - item: item, + params: params, animation: .None, synchronously: true, animateContents: false, @@ -2326,7 +2371,7 @@ final class TextContentItemLayer: SimpleLayer { animation.animator.updateBounds(layer: blockExpandArrow, bounds: CGRect(origin: CGPoint(), size: expandArrowFrame.size), completion: nil) animation.animator.updateTransform(layer: blockExpandArrow, transform: CATransform3DMakeRotation(isCollapsed ? 0.0 : CGFloat.pi, 0.0, 0.0, 1.0), completion: nil) - let contentMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: blockBackgroundFrame.minY - contentFrame.minY), size: CGSize(width: contentFrame.width, height: blockBackgroundFrame.height)) + let contentMaskFrame = CGRect(origin: CGPoint(x: 0.0, y: contentFrame.minY - blockBackgroundFrame.minY), size: CGSize(width: contentFrame.width, height: blockBackgroundFrame.height)) contentMask = RenderMask(image: expandableBlockMaskImage, isOpaque: !isCollapsed, frame: contentMaskFrame) effectiveContentFrame.size.height = ceil(contentMaskFrame.height - contentMaskFrame.minY) } else { @@ -2391,44 +2436,221 @@ final class TextContentItemLayer: SimpleLayer { } } + if !params.item.segment.spoilers.isEmpty { + let spoilerEffectNode: InvisibleInkDustNode + if let current = self.spoilerEffectNode { + spoilerEffectNode = current + } else { + spoilerEffectNode = InvisibleInkDustNode(textNode: nil, enableAnimations: params.areContentAnimationsEnabled) + self.spoilerEffectNode = spoilerEffectNode + } + + spoilerEffectNode.frame = contentFrame + spoilerEffectNode.update( + size: contentFrame.size, + color: params.spoilerEffectColor, + textColor: params.spoilerTextColor, + rects: params.item.segment.spoilers.map { $0.1.offsetBy(dx: 0.0 + params.item.contentOffset.x, dy: params.item.contentOffset.y + 0.0).insetBy(dx: 1.0, dy: 1.0) }, + wordRects: params.item.segment.spoilerWords.map { $0.1.offsetBy(dx: params.item.contentOffset.x + 0.0, dy: params.item.contentOffset.y + 0.0).insetBy(dx: 1.0, dy: 1.0) } + ) + } else { + if let spoilerEffectNode = self.spoilerEffectNode { + self.spoilerEffectNode = nil + spoilerEffectNode.layer.removeFromSuperlayer() + } + } + + if self.spoilerEffectNode != nil { + let overlayContentLayer: SimpleLayer + if let current = self.overlayContentLayer { + overlayContentLayer = current + animation.animator.updateFrame(layer: overlayContentLayer, frame: effectiveContentFrame, completion: nil) + } else { + overlayContentLayer = SimpleLayer() + self.overlayContentLayer = overlayContentLayer + overlayContentLayer.masksToBounds = true + self.addSublayer(overlayContentLayer) + overlayContentLayer.frame = effectiveContentFrame + } + + if let spoilerEffectNode = self.spoilerEffectNode { + if spoilerEffectNode.layer.superlayer !== overlayContentLayer { + overlayContentLayer.addSublayer(spoilerEffectNode.layer) + } + } + } else { + if let overlayContentLayer = self.overlayContentLayer { + self.overlayContentLayer = nil + overlayContentLayer.removeFromSuperlayer() + } + } + self.currentContentMask = contentMask - self.renderNode.params = RenderParams(size: contentFrame.size, item: item, mask: staticContentMask) + self.renderNode.params = RenderParams(size: contentFrame.size, item: params.item, mask: staticContentMask) if synchronously { - if let spoilerExpandRect { - let _ = spoilerExpandRect + if let spoilerExpandRect, animation.isAnimated { + let localSpoilerExpandRect = spoilerExpandRect.offsetBy(dx: -self.renderNode.frame.minX, dy: -self.renderNode.frame.minY) + + let revealAnimationDuration: CGFloat = 0.55 + + let revealTransition: ContainedViewLayoutTransition = .animated(duration: revealAnimationDuration, curve: .easeInOut) + + let previousContents = self.renderNode.layer.contents + let copyContentsLayer = SimpleLayer() + copyContentsLayer.frame = self.renderNode.frame + copyContentsLayer.contents = previousContents + copyContentsLayer.masksToBounds = self.renderNode.layer.masksToBounds + copyContentsLayer.contentsGravity = self.renderNode.layer.contentsGravity + copyContentsLayer.contentsScale = self.renderNode.layer.contentsScale + for sublayer in self.renderNode.layer.sublayers ?? [] { + let copySublayer = SimpleLayer() + copySublayer.contentsScale = sublayer.contentsScale + copySublayer.position = sublayer.position + copySublayer.bounds = sublayer.bounds + copySublayer.transform = sublayer.transform + copySublayer.opacity = sublayer.opacity + copySublayer.isHidden = sublayer.isHidden + + if let sublayer = sublayer as? InlineStickerItemLayer { + sublayer.mirrorLayer = copySublayer + } else { + copySublayer.contents = sublayer.contents + } + + copyContentsLayer.addSublayer(copySublayer) + } + self.renderNode.layer.superlayer?.insertSublayer(copyContentsLayer, below: self.renderNode.layer) self.renderNode.displayImmediately() - let maskFrame = self.renderNode.frame + let rectangularExpandedSide = max(localSpoilerExpandRect.width, localSpoilerExpandRect.height) + // The gradient starts at 0.7 + let adjustedExpandedSide = ceil(rectangularExpandedSide * 1.3) + + let rectangularExpandedRect = CGSize(width: adjustedExpandedSide, height: adjustedExpandedSide).centered(around: spoilerExpandRect.center) + + let maskFrame = self.renderNode.bounds let maskLayer = SimpleLayer() + maskLayer.masksToBounds = true + self.renderNode.layer.mask = maskLayer maskLayer.frame = maskFrame - self.addSublayer(maskLayer) - let maskGradientLayer = SimpleGradientLayer() - maskGradientLayer.frame = CGRect(origin: CGPoint(), size: maskFrame.size) - setupSpoilerExpansionMaskGradient( - gradientLayer: maskGradientLayer, - centerLocation: CGPoint( - x: 0.5, - y: 0.5 - ), - radius: CGSize( - width: 1.5, - height: 1.5 - ), - inverse: false - ) + animateRadialExpansionMask(maskLayer: maskLayer, expandedRect: rectangularExpandedRect, transition: revealTransition, inverse: false, completion: { [weak self] in + guard let self, let params = self.params else { + return + } + self.renderNode.layer.mask = nil + self.update( + params: params, + animation: .None, + synchronously: true, + animateContents: false, + spoilerExpandRect: nil + ) + }) + + let copyMaskLayer = SimpleLayer() + copyMaskLayer.masksToBounds = true + copyContentsLayer.mask = copyMaskLayer + copyMaskLayer.frame = maskFrame + + animateRadialExpansionMask(maskLayer: copyMaskLayer, expandedRect: rectangularExpandedRect, transition: revealTransition, inverse: true, completion: { [weak copyContentsLayer] in + copyContentsLayer?.removeFromSuperlayer() + }) + + if let spoilerEffectNode = self.spoilerEffectNode { + let spoilerMaskLayer = SimpleLayer() + spoilerMaskLayer.masksToBounds = true + spoilerEffectNode.layer.mask = spoilerMaskLayer + spoilerMaskLayer.frame = maskFrame + + let spoilerLocalPosition = self.convert(rectangularExpandedRect.center, to: spoilerEffectNode.layer) + spoilerEffectNode.revealWithoutMaskAtLocation(spoilerLocalPosition) + + animateRadialExpansionMask(maskLayer: spoilerMaskLayer, expandedRect: rectangularExpandedRect, transition: revealTransition, inverse: true, completion: { [weak self] in + guard let self, let spoilerEffectNode = self.spoilerEffectNode else { + return + } + spoilerEffectNode.layer.mask = nil + spoilerEffectNode.layer.opacity = 0.0 + }) + } } else { let previousContents = self.renderNode.layer.contents self.renderNode.displayImmediately() if animateContents, let previousContents { animation.transition.animateContents(layer: self.renderNode.layer, from: previousContents) } + + if let spoilerEffectNode = self.spoilerEffectNode { + animation.transition.updateAlpha(layer: spoilerEffectNode.layer, alpha: params.item.displayContentsUnderSpoilers ? 0.0 : 1.0) + } } } else { self.renderNode.setNeedsDisplay() + + if let spoilerEffectNode = self.spoilerEffectNode { + animation.transition.updateAlpha(layer: spoilerEffectNode.layer, alpha: params.item.displayContentsUnderSpoilers ? 0.0 : 1.0) + } + } + } +} + +private func animateRadialExpansionMask(maskLayer: CALayer, expandedRect: CGRect, transition: ContainedViewLayoutTransition, inverse: Bool, completion: @escaping () -> Void) { + let maskGradientLayer = SimpleGradientLayer() + maskLayer.addSublayer(maskGradientLayer) + maskGradientLayer.frame = expandedRect + + setupSpoilerExpansionMaskGradient( + gradientLayer: maskGradientLayer, + centerLocation: CGPoint( + x: 0.5, + y: 0.5 + ), + radius: CGSize( + width: 0.5, + height: 0.5 + ), + inverse: inverse + ) + + let minGradientFrame = CGSize(width: 1.0, height: 1.0).centered(around: expandedRect.center) + + transition.animateFrame(layer: maskGradientLayer, from: minGradientFrame, delay: 0.1, completion: { _ in + completion() + }) + + if inverse { + let outerBoundsSourceRect = minGradientFrame.insetBy(dx: 0.5, dy: 0.5) + let outerBoundsDestinationRect = expandedRect.insetBy(dx: 0.5, dy: 0.5) + + for sideIndex in 0 ..< 4 { + let copyMaskOuterBoundsTopLayer = SimpleLayer() + copyMaskOuterBoundsTopLayer.backgroundColor = UIColor.white.cgColor + maskLayer.addSublayer(copyMaskOuterBoundsTopLayer) + + let sourceFrame: CGRect + let destinationFrame: CGRect + + // Top, left, bottom, right + if sideIndex == 0 { + sourceFrame = CGRect(origin: CGPoint(x: 0.0, y: outerBoundsSourceRect.minY - expandedRect.height), size: expandedRect.size) + destinationFrame = CGRect(origin: CGPoint(x: 0.0, y: outerBoundsDestinationRect.minY - expandedRect.height), size: expandedRect.size) + } else if sideIndex == 1 { + sourceFrame = CGRect(origin: CGPoint(x: outerBoundsSourceRect.minX - expandedRect.width, y: 0.0), size: expandedRect.size) + destinationFrame = CGRect(origin: CGPoint(x: outerBoundsDestinationRect.minX - expandedRect.width, y: 0.0), size: expandedRect.size) + } else if sideIndex == 2 { + sourceFrame = CGRect(origin: CGPoint(x: 0.0, y: outerBoundsSourceRect.maxY), size: expandedRect.size) + destinationFrame = CGRect(origin: CGPoint(x: 0.0, y: outerBoundsDestinationRect.maxY), size: expandedRect.size) + } else { + sourceFrame = CGRect(origin: CGPoint(x: outerBoundsSourceRect.maxX, y: 0.0), size: expandedRect.size) + destinationFrame = CGRect(origin: CGPoint(x: outerBoundsDestinationRect.maxX, y: 0.0), size: expandedRect.size) + } + + copyMaskOuterBoundsTopLayer.frame = destinationFrame + transition.animateFrame(layer: copyMaskOuterBoundsTopLayer, from: sourceFrame, delay: 0.1) } } } diff --git a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift index 97ae6c2c1f..cb92c3f9cb 100644 --- a/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/InteractiveTextComponent/Sources/InteractiveTextNodeWithEntities.swift @@ -11,7 +11,6 @@ import AnimationCache import MultiAnimationRenderer import TelegramCore import EmojiTextAttachmentView -import InvisibleInkDustNode private final class InlineStickerItem: Hashable { let emoji: ChatTextInputTextCustomEmojiAttribute @@ -64,8 +63,7 @@ public final class InteractiveTextNodeWithEntities { public let attemptSynchronous: Bool public let textColor: UIColor public let spoilerEffectColor: UIColor - public let animation: ListViewItemUpdateAnimation - public let animationArguments: InteractiveTextNode.AnimationArguments? + public let applyArguments: InteractiveTextNode.ApplyArguments public init( context: AccountContext, @@ -75,8 +73,7 @@ public final class InteractiveTextNodeWithEntities { attemptSynchronous: Bool, textColor: UIColor, spoilerEffectColor: UIColor, - animation: ListViewItemUpdateAnimation, - animationArguments: InteractiveTextNode.AnimationArguments? + applyArguments: InteractiveTextNode.ApplyArguments ) { self.context = context self.cache = cache @@ -85,8 +82,7 @@ public final class InteractiveTextNodeWithEntities { self.attemptSynchronous = attemptSynchronous self.textColor = textColor self.spoilerEffectColor = spoilerEffectColor - self.animation = animation - self.animationArguments = animationArguments + self.applyArguments = applyArguments } public func withUpdatedPlaceholderColor(_ color: UIColor) -> Arguments { @@ -98,8 +94,7 @@ public final class InteractiveTextNodeWithEntities { attemptSynchronous: self.attemptSynchronous, textColor: self.textColor, spoilerEffectColor: self.spoilerEffectColor, - animation: self.animation, - animationArguments: self.animationArguments + applyArguments: self.applyArguments ) } } @@ -116,7 +111,6 @@ public final class InteractiveTextNodeWithEntities { public let textNode: InteractiveTextNode private var inlineStickerItemLayers: [InlineStickerItemLayer.Key: InlineStickerItemLayerData] = [:] - private var dustEffectNodes: [Int: InvisibleInkDustNode] = [:] private var displayContentsUnderSpoilers: Bool? private var enableLooping: Bool = true @@ -149,7 +143,7 @@ public final class InteractiveTextNodeWithEntities { self.textNode = textNode } - public static func asyncLayout(_ maybeNode: InteractiveTextNodeWithEntities?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (InteractiveTextNodeWithEntities.Arguments?) -> InteractiveTextNodeWithEntities) { + public static func asyncLayout(_ maybeNode: InteractiveTextNodeWithEntities?) -> (InteractiveTextNodeLayoutArguments) -> (InteractiveTextNodeLayout, (InteractiveTextNodeWithEntities.Arguments) -> InteractiveTextNodeWithEntities) { let makeLayout = InteractiveTextNode.asyncLayout(maybeNode?.textNode) return { [weak maybeNode] arguments in var updatedString: NSAttributedString? @@ -218,44 +212,40 @@ public final class InteractiveTextNodeWithEntities { let (layout, apply) = makeLayout(arguments.withAttributedString(updatedString)) return (layout, { applyArguments in - let animation: ListViewItemUpdateAnimation = applyArguments?.animation ?? .None + let animation: ListViewItemUpdateAnimation = applyArguments.applyArguments.animation - let result = apply(animation, applyArguments?.animationArguments) + let result = apply(applyArguments.applyArguments) if let maybeNode = maybeNode { - if let applyArguments = applyArguments { - maybeNode.updateInteractiveContents( - context: applyArguments.context, - cache: applyArguments.cache, - renderer: applyArguments.renderer, - textLayout: layout, - placeholderColor: applyArguments.placeholderColor, - attemptSynchronousLoad: false, - textColor: applyArguments.textColor, - spoilerEffectColor: applyArguments.spoilerEffectColor, - animation: animation, - animationArguments: applyArguments.animationArguments - ) - } + maybeNode.updateInteractiveContents( + context: applyArguments.context, + cache: applyArguments.cache, + renderer: applyArguments.renderer, + textLayout: layout, + placeholderColor: applyArguments.placeholderColor, + attemptSynchronousLoad: false, + textColor: applyArguments.textColor, + spoilerEffectColor: applyArguments.spoilerEffectColor, + animation: animation, + applyArguments: applyArguments.applyArguments + ) return maybeNode } else { let resultNode = InteractiveTextNodeWithEntities(textNode: result) - if let applyArguments = applyArguments { - resultNode.updateInteractiveContents( - context: applyArguments.context, - cache: applyArguments.cache, - renderer: applyArguments.renderer, - textLayout: layout, - placeholderColor: applyArguments.placeholderColor, - attemptSynchronousLoad: false, - textColor: applyArguments.textColor, - spoilerEffectColor: applyArguments.spoilerEffectColor, - animation: .None, - animationArguments: nil - ) - } + resultNode.updateInteractiveContents( + context: applyArguments.context, + cache: applyArguments.cache, + renderer: applyArguments.renderer, + textLayout: layout, + placeholderColor: applyArguments.placeholderColor, + attemptSynchronousLoad: false, + textColor: applyArguments.textColor, + spoilerEffectColor: applyArguments.spoilerEffectColor, + animation: .None, + applyArguments: applyArguments.applyArguments + ) return resultNode } @@ -281,7 +271,7 @@ public final class InteractiveTextNodeWithEntities { textColor: UIColor, spoilerEffectColor: UIColor, animation: ListViewItemUpdateAnimation, - animationArguments: InteractiveTextNode.AnimationArguments? + applyArguments: InteractiveTextNode.ApplyArguments ) { self.enableLooping = context.sharedContext.energyUsageSettings.loopEmoji @@ -289,18 +279,16 @@ public final class InteractiveTextNodeWithEntities { if let textLayout { displayContentsUnderSpoilers = textLayout.displayContentsUnderSpoilers } - let previousDisplayContentsUnderSpoilers = self.displayContentsUnderSpoilers + self.displayContentsUnderSpoilers = displayContentsUnderSpoilers var nextIndexById: [Int64: Int] = [:] var validIds: [InlineStickerItemLayer.Key] = [] - var validDustEffectIds: [Int] = [] - if let textLayout { for i in 0 ..< textLayout.segments.count { let segment = textLayout.segments[i] - guard let segmentLayer = self.textNode.segmentLayer(index: i), let segmentItem = segmentLayer.item else { + guard let segmentLayer = self.textNode.segmentLayer(index: i), let segmentParams = segmentLayer.params else { continue } @@ -322,8 +310,8 @@ public final class InteractiveTextNodeWithEntities { itemFrame.origin.x = floorToScreenPixels(itemFrame.origin.x) itemFrame.origin.y = floorToScreenPixels(itemFrame.origin.y) - itemFrame.origin.x += segmentItem.contentOffset.x - itemFrame.origin.y += segmentItem.contentOffset.y + itemFrame.origin.x += segmentParams.item.contentOffset.x + itemFrame.origin.y += segmentParams.item.contentOffset.y let itemLayerData: InlineStickerItemLayerData var itemLayerTransition = animation.transition @@ -341,45 +329,13 @@ public final class InteractiveTextNodeWithEntities { self.inlineStickerItemLayers[id] = itemLayerData segmentLayer.renderNode.layer.addSublayer(itemLayerData.itemLayer) - itemLayerData.itemLayer.isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame.offsetBy(dx: -segmentItem.contentOffset.x, dy: -segmentItem.contentOffset.x)) + itemLayerData.itemLayer.isVisibleForAnimations = self.enableLooping && self.isItemVisible(itemRect: itemFrame.offsetBy(dx: -segmentParams.item.contentOffset.x, dy: -segmentParams.item.contentOffset.x)) } itemLayerTransition.updateAlpha(layer: itemLayerData.itemLayer, alpha: item.isHiddenBySpoiler ? 0.0 : 1.0) itemLayerData.itemLayer.frame = itemFrame - itemLayerData.rect = itemFrame.offsetBy(dx: -segmentItem.contentOffset.x, dy: -segmentItem.contentOffset.y) - } - } - - if !segment.spoilers.isEmpty { - validDustEffectIds.append(i) - - let dustEffectNode: InvisibleInkDustNode - if let current = self.dustEffectNodes[i] { - dustEffectNode = current - if dustEffectNode.layer.superlayer !== segmentLayer.renderNode.layer { - segmentLayer.renderNode.layer.addSublayer(dustEffectNode.layer) - } - } else { - dustEffectNode = InvisibleInkDustNode(textNode: nil, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency) - self.dustEffectNodes[i] = dustEffectNode - segmentLayer.renderNode.layer.addSublayer(dustEffectNode.layer) - } - let dustNodeFrame = CGRect(origin: CGPoint(), size: segmentItem.size).insetBy(dx: -3.0, dy: -3.0) - dustEffectNode.frame = dustNodeFrame - dustEffectNode.update( - size: dustNodeFrame.size, - color: spoilerEffectColor, - textColor: textColor, - rects: segment.spoilers.map { $0.1.offsetBy(dx: 3.0 + segmentItem.contentOffset.x, dy: segmentItem.contentOffset.y + 3.0).insetBy(dx: 1.0, dy: 1.0) }, - wordRects: segment.spoilerWords.map { $0.1.offsetBy(dx: segmentItem.contentOffset.x + 3.0, dy: segmentItem.contentOffset.y + 3.0).insetBy(dx: 1.0, dy: 1.0) } - ) - - if let previousDisplayContentsUnderSpoilers, previousDisplayContentsUnderSpoilers != displayContentsUnderSpoilers, displayContentsUnderSpoilers, let currentSpoilerExpandRect = animationArguments?.spoilerExpandRect { - let spoilerLocalPosition = self.textNode.layer.convert(currentSpoilerExpandRect.center, to: dustEffectNode.layer) - dustEffectNode.revealAtLocation(spoilerLocalPosition) - } else { - dustEffectNode.update(revealed: displayContentsUnderSpoilers, animated: previousDisplayContentsUnderSpoilers != nil && animation.isAnimated) + itemLayerData.rect = itemFrame.offsetBy(dx: -segmentParams.item.contentOffset.x, dy: -segmentParams.item.contentOffset.y) } } } @@ -395,16 +351,5 @@ public final class InteractiveTextNodeWithEntities { for key in removeKeys { self.inlineStickerItemLayers.removeValue(forKey: key) } - - var removeDustEffectIds: [Int] = [] - for (id, dustEffectNode) in self.dustEffectNodes { - if !validDustEffectIds.contains(id) { - removeDustEffectIds.append(id) - dustEffectNode.removeFromSupernode() - } - } - for id in removeDustEffectIds { - self.dustEffectNodes.removeValue(forKey: id) - } } }