diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index eaec2901f2..d809b98a6f 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -878,6 +878,8 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { public init(context: AccountContext, subject: PremiumDemoScreen.Subject, source: PremiumDemoScreen.Source = .other, action: @escaping () -> Void) { super.init(context: context, component: DemoSheetComponent(context: context, subject: subject, source: source, action: action), navigationBarAppearance: .none) + self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + self.navigationPresentation = .flatModal } diff --git a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift index 788a7d2f4f..2a9a190785 100644 --- a/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift +++ b/submodules/PremiumUI/Sources/ReactionsCarouselComponent.swift @@ -1,6 +1,7 @@ import Foundation import UIKit import Display +import SwiftSignalKit import AsyncDisplayKit import ComponentFlow import TelegramCore @@ -66,7 +67,9 @@ final class ReactionsCarouselComponent: Component { } if isDisplaying && !self.isVisible { - self.node?.animateIn() + self.node?.setVisible(true) + } else if !isDisplaying && self.isVisible { + self.node?.setVisible(false) } self.isVisible = isDisplaying @@ -85,6 +88,9 @@ final class ReactionsCarouselComponent: Component { private let itemSize = CGSize(width: 110.0, height: 110.0) +//private let order = ["👌","😍","🤡","🕊","🥱","🥴"] +private let order = ["😍","👌","🥴","🥱","🕊","🤡"] + private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { private let context: AccountContext private let theme: PresentationTheme @@ -105,10 +111,27 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { private let positionDelta: Double + private var previousInteractionTimestamp: Double = 0.0 + private var timer: SwiftSignalKit.Timer? + private var hasIdleAnimations = false + init(context: AccountContext, theme: PresentationTheme, reactions: [AvailableReactions.Reaction]) { self.context = context self.theme = theme - self.reactions = Array(reactions.shuffled().prefix(6)) + + var reactionMap: [String: AvailableReactions.Reaction] = [:] + for reaction in reactions { + reactionMap[reaction.value] = reaction + } + + var sortedReactions: [AvailableReactions.Reaction] = [] + for emoji in order { + if let reaction = reactionMap[emoji] { + sortedReactions.append(reaction) + } + } + + self.reactions = sortedReactions self.scrollNode = ASScrollNode() self.tapNode = ASDisplayNode() @@ -123,6 +146,10 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { self.setup() } + deinit { + self.timer?.invalidate() + } + override func didLoad() { super.didLoad() @@ -134,6 +161,8 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { } @objc private func reactionTapped(_ gestureRecognizer: UITapGestureRecognizer) { + self.previousInteractionTimestamp = CACurrentMediaTime() + guard self.animator == nil, self.scrollStartPosition == nil else { return } @@ -143,11 +172,42 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { return } - self.scrollTo(index, playReaction: true, duration: 0.4) + self.scrollTo(index, playReaction: true, immediately: true, duration: 0.4) + } + + func setVisible(_ visible: Bool) { + if visible { + self.animateIn() + } else { + self.scrollTo(0, playReaction: false, immediately: false, duration: 0.0, clockwise: false) + self.timer?.invalidate() + self.timer = nil + + self.playingIndices.removeAll() + self.standaloneReactionAnimation?.removeFromSupernode() + } } func animateIn() { - self.scrollTo(1, playReaction: true, duration: 0.5, clockwise: true) + self.scrollTo(1, playReaction: true, immediately: false, duration: 0.5, clockwise: true) + + if self.timer == nil { + self.previousInteractionTimestamp = CACurrentMediaTime() + self.timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + if let strongSelf = self { + let currentTimestamp = CACurrentMediaTime() + if currentTimestamp > strongSelf.previousInteractionTimestamp + 5.0 { + var nextIndex = strongSelf.currentIndex - 1 + if nextIndex < 0 { + nextIndex = strongSelf.reactions.count + nextIndex + } + strongSelf.scrollTo(nextIndex, playReaction: true, immediately: true, duration: 0.3, clockwise: true) + strongSelf.previousInteractionTimestamp = currentTimestamp + } + } + }, queue: Queue.mainQueue()) + self.timer?.start() + } } func animateOut() { @@ -156,7 +216,7 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { } } - func scrollTo(_ index: Int, playReaction: Bool, duration: Double, clockwise: Bool? = nil) { + func scrollTo(_ index: Int, playReaction: Bool, immediately: Bool, duration: Double, clockwise: Bool? = nil) { guard index >= 0 && index < self.itemNodes.count else { return } @@ -184,25 +244,36 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { } } - self.animator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] t in - let t = listViewAnimationCurveSystem(t) - var updatedPosition = startPosition + change * t - while updatedPosition >= 1.0 { - updatedPosition -= 1.0 + if immediately { + self.playReaction(index: index) + } + + if duration.isZero { + self.currentPosition = newPosition + if let size = self.validLayout { + self.updateLayout(size: size, transition: .immediate) } - while updatedPosition < 0.0 { - updatedPosition += 1.0 - } - self?.currentPosition = updatedPosition - if let size = self?.validLayout { - self?.updateLayout(size: size, transition: .immediate) - } - }, completion: { [weak self] in - self?.animator = nil - if playReaction { - self?.playReaction() - } - }) + } else { + self.animator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] t in + let t = listViewAnimationCurveSystem(t) + var updatedPosition = startPosition + change * t + while updatedPosition >= 1.0 { + updatedPosition -= 1.0 + } + while updatedPosition < 0.0 { + updatedPosition += 1.0 + } + self?.currentPosition = updatedPosition + if let size = self?.validLayout { + self?.updateLayout(size: size, transition: .immediate) + } + }, completion: { [weak self] in + self?.animator = nil + if playReaction && !immediately { + self?.playReaction(index: nil) + } + }) + } } func setup() { @@ -240,14 +311,19 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { self.ignoreContentOffsetChange = false } - func playReaction() { - let delta = self.positionDelta - let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count) + func playReaction(index: Int?) { + let index = index ?? max(0, Int(round(self.currentPosition / self.positionDelta)) % self.itemNodes.count) guard !self.playingIndices.contains(index) else { return } + if let current = self.standaloneReactionAnimation, let dismiss = current.currentDismissAnimation { + dismiss() + current.currentDismissAnimation = nil + self.playingIndices.removeAll() + } + let reaction = self.reactions[index] let targetContainerNode = self.itemContainerNodes[index] let targetView = self.itemNodes[index].view @@ -284,10 +360,13 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { forceSmallEffectAnimation: true, targetView: targetView, addStandaloneReactionAnimation: nil, + currentItemNode: self.itemNodes[index], completion: { [weak standaloneReactionAnimation, weak self] in standaloneReactionAnimation?.removeFromSupernode() - self?.standaloneReactionAnimation = nil - self?.playingIndices.remove(index) + if self?.standaloneReactionAnimation === standaloneReactionAnimation { + self?.standaloneReactionAnimation = nil + self?.playingIndices.remove(index) + } } ) } @@ -301,6 +380,10 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { private let hapticFeedback = HapticFeedback() func scrollViewDidScroll(_ scrollView: UIScrollView) { + if scrollView.isTracking { + self.previousInteractionTimestamp = CACurrentMediaTime() + } + guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else { return } @@ -347,17 +430,21 @@ private class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate { func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { + self.previousInteractionTimestamp = CACurrentMediaTime() + self.resetScrollPosition() let delta = self.positionDelta let index = max(0, Int(round(self.currentPosition / delta)) % self.itemNodes.count) - self.scrollTo(index, playReaction: true, duration: 0.2) + self.scrollTo(index, playReaction: true, immediately: true, duration: 0.2) } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.previousInteractionTimestamp = CACurrentMediaTime() + self.resetScrollPosition() - self.playReaction() + self.playReaction(index: nil) } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 941e967abf..0a6a09a083 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -936,7 +936,9 @@ public final class StandaloneReactionAnimation: ASDisplayNode { self.animateReactionSelection(context: context, theme: theme, reaction: reaction, avatarPeers: avatarPeers, playHaptic: playHaptic, isLarge: isLarge, forceSmallEffectAnimation: forceSmallEffectAnimation, targetView: targetView, addStandaloneReactionAnimation: addStandaloneReactionAnimation, currentItemNode: nil, completion: completion) } - func animateReactionSelection(context: AccountContext, theme: PresentationTheme, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, currentItemNode: ReactionNode?, completion: @escaping () -> Void) { + public var currentDismissAnimation: (() -> Void)? + + public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, currentItemNode: ReactionNode?, completion: @escaping () -> Void) { guard let sourceSnapshotView = targetView.snapshotContentTree() else { completion() return @@ -955,12 +957,14 @@ public final class StandaloneReactionAnimation: ASDisplayNode { itemNode = ReactionNode(context: context, theme: theme, item: reaction) } self.itemNode = itemNode - - if let targetView = targetView as? ReactionIconView, !isLarge { - self.itemNodeIsEmbedded = true - targetView.addSubnode(itemNode) - } else { - self.addSubnode(itemNode) + + if !forceSmallEffectAnimation { + if let targetView = targetView as? ReactionIconView, !isLarge { + self.itemNodeIsEmbedded = true + targetView.addSubnode(itemNode) + } else { + self.addSubnode(itemNode) + } } itemNode.expandedAnimationDidBegin = { [weak self, weak targetView] in @@ -975,7 +979,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { targetView.isHidden = true } } - + itemNode.isExtracted = true let selfTargetRect = self.view.convert(targetView.bounds, from: targetView) @@ -1077,7 +1081,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { completion() } } - + var didBeginDismissAnimation = false let beginDismissAnimation: () -> Void = { [weak self] in if !didBeginDismissAnimation { @@ -1089,62 +1093,91 @@ public final class StandaloneReactionAnimation: ASDisplayNode { return } - if isLarge { - strongSelf.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: true, completion: { - if let addStandaloneReactionAnimation = addStandaloneReactionAnimation { - let standaloneReactionAnimation = StandaloneReactionAnimation() + if forceSmallEffectAnimation { + additionalAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak additionalAnimationNode] _ in + additionalAnimationNode?.removeFromSupernode() + }) + + mainAnimationCompleted = true + intermediateCompletion() + } else { + if isLarge { + strongSelf.animateFromItemNodeToReaction(itemNode: itemNode, targetView: targetView, hideNode: true, completion: { + if let addStandaloneReactionAnimation = addStandaloneReactionAnimation { + let standaloneReactionAnimation = StandaloneReactionAnimation() + + addStandaloneReactionAnimation(standaloneReactionAnimation) + + standaloneReactionAnimation.animateReactionSelection( + context: itemNode.context, + theme: itemNode.context.sharedContext.currentPresentationData.with({ $0 }).theme, + reaction: itemNode.item, + avatarPeers: avatarPeers, + playHaptic: false, + isLarge: false, + targetView: targetView, + addStandaloneReactionAnimation: nil, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) + } - addStandaloneReactionAnimation(standaloneReactionAnimation) - - standaloneReactionAnimation.animateReactionSelection( - context: itemNode.context, - theme: itemNode.context.sharedContext.currentPresentationData.with({ $0 }).theme, - reaction: itemNode.item, - avatarPeers: avatarPeers, - playHaptic: false, - isLarge: false, - targetView: targetView, - addStandaloneReactionAnimation: nil, - completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() - } - ) + mainAnimationCompleted = true + intermediateCompletion() + }) + } else { + if let targetView = strongSelf.targetView { + if let targetView = targetView as? ReactionIconView, !isLarge { + targetView.imageView.isHidden = false + } else { + targetView.alpha = 1.0 + targetView.isHidden = false + } + } + + if strongSelf.itemNodeIsEmbedded { + strongSelf.itemNode?.removeFromSupernode() } mainAnimationCompleted = true intermediateCompletion() - }) - } else { - if let targetView = strongSelf.targetView { - if let targetView = targetView as? ReactionIconView, !isLarge { - targetView.imageView.isHidden = false - } else { - targetView.alpha = 1.0 - targetView.isHidden = false - } } - - if strongSelf.itemNodeIsEmbedded { - strongSelf.itemNode?.removeFromSupernode() - } - - mainAnimationCompleted = true - intermediateCompletion() } } } + self.currentDismissAnimation = beginDismissAnimation + let maybeBeginDismissAnimation: () -> Void = { + if mainAnimationCompleted && additionalAnimationCompleted { + beginDismissAnimation() + } + } + + if forceSmallEffectAnimation { + itemNode.mainAnimationCompletion = { + mainAnimationCompleted = true + maybeBeginDismissAnimation() + } + } + additionalAnimationNode.completed = { _ in additionalAnimationCompleted = true intermediateCompletion() - beginDismissAnimation() + if forceSmallEffectAnimation { + maybeBeginDismissAnimation() + } else { + beginDismissAnimation() + } } additionalAnimationNode.visibility = true - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: { - beginDismissAnimation() - }) + if !forceSmallEffectAnimation { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0, execute: { + beginDismissAnimation() + }) + } } private func animateFromItemNodeToReaction(itemNode: ReactionNode, targetView: UIView, hideNode: Bool, completion: @escaping () -> Void) { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index e57be1642b..a9976646ee 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -45,6 +45,7 @@ protocol ReactionItemNode: ASDisplayNode { public final class ReactionNode: ASDisplayNode, ReactionItemNode { let context: AccountContext let item: ReactionItem + private let hasAppearAnimation: Bool private var animateInAnimationNode: AnimatedStickerNode? private let staticAnimationNode: AnimatedStickerNode @@ -67,6 +68,7 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, hasAppearAnimation: Bool = true) { self.context = context self.item = item + self.hasAppearAnimation = hasAppearAnimation self.staticAnimationNode = AnimatedStickerNode() @@ -113,6 +115,8 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { } } + public var mainAnimationCompletion: (() -> Void)? + public func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) { let intrinsicSize = size @@ -130,7 +134,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { let expandedAnimationFrame = animationFrame - if isExpanded, self.animationNode == nil { + if isExpanded && !self.hasAppearAnimation { + self.staticAnimationNode.play(fromIndex: 0) + } else if isExpanded, self.animationNode == nil { let animationNode = AnimatedStickerNode() animationNode.automaticallyLoadFirstFrame = true self.animationNode = animationNode @@ -143,6 +149,9 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { self?.expandedAnimationDidBegin?() } } + animationNode.completed = { [weak self] _ in + self?.mainAnimationCompletion?() + } if largeExpanded { animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource), width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .once, mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.largeListAnimation.resource.id))) @@ -274,7 +283,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { if self.animationNode == nil { self.didSetupStillAnimation = true - self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) + if !self.hasAppearAnimation { + self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.largeListAnimation.resource), width: Int(expandedAnimationFrame.width * 2.0), height: Int(expandedAnimationFrame.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.largeListAnimation.resource.id))) + } else { + self.staticAnimationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: self.item.stillAnimation.resource), width: Int(animationDisplaySize.width * 2.0), height: Int(animationDisplaySize.height * 2.0), playbackMode: .still(.start), mode: .direct(cachePathPrefix: self.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(self.item.stillAnimation.resource.id))) + } self.staticAnimationNode.position = animationFrame.center self.staticAnimationNode.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) self.staticAnimationNode.updateLayout(size: animationFrame.size) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 7cd9e1fbf1..680db8f8dd 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1044,11 +1044,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !reaction.isEnabled { continue } - if reaction.isPremium && !hasPremium { - hasPremiumPlaceholder = true - continue - } - + switch allowedReactions { case let .set(set): if !set.contains(reaction.value) { @@ -1057,6 +1053,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G case .all: break } + + if reaction.isPremium && !hasPremium { + hasPremiumPlaceholder = true + continue + } + actions.reactionItems.append(.reaction(ReactionItem( reaction: ReactionItem.Reaction(rawValue: reaction.value), appearAnimation: reaction.appearAnimation,