diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index afeb4c0f9a..3602be9e48 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -6721,6 +6721,8 @@ Sorry for the inconvenience."; "Conversation.Theme.DontSetTheme" = "Do Not Set Theme"; "Conversation.Theme.SwitchToDark" = "Switch to dark appearance"; "Conversation.Theme.SwitchToLight" = "Switch to light appearance"; +"Conversation.Theme.DismissAlert" = "Do you want to apply the selected theme to the chat?"; +"Conversation.Theme.DismissAlertApply" = "Apply"; "Notification.ChangedTheme" = "%1$@ changed chat theme to %2$@"; "Notification.DisabledTheme" = "%@ disabled chat theme"; diff --git a/submodules/AnimationUI/Sources/AnimationNode.swift b/submodules/AnimationUI/Sources/AnimationNode.swift index a034b855d5..edc3aa69c7 100644 --- a/submodules/AnimationUI/Sources/AnimationNode.swift +++ b/submodules/AnimationUI/Sources/AnimationNode.swift @@ -24,8 +24,11 @@ public final class AnimationNode : ASDisplayNode { return self.animationView()?.isAnimationPlaying ?? false } + private var currentParams: (String?, [String: UIColor]?)? + public init(animation: String? = nil, colors: [String: UIColor]? = nil, scale: CGFloat = 1.0) { self.scale = scale + self.currentParams = (animation, colors) super.init() @@ -80,11 +83,23 @@ public final class AnimationNode : ASDisplayNode { }) } + public func makeCopy(colors: [String: UIColor]? = nil, progress: CGFloat? = nil) -> AnimationNode? { + guard let (animation, currentColors) = self.currentParams else { + return nil + } + let animationNode = AnimationNode(animation: animation, colors: colors ?? currentColors, scale: 1.0) + animationNode.animationView()?.play(fromProgress: progress ?? (self.animationView()?.animationProgress ?? 0.0), toProgress: 1.0, withCompletion: { [weak animationNode] _ in + animationNode?.completion?() + }) + return animationNode + } + public func seekToEnd() { self.animationView()?.animationProgress = 1.0 } public func setAnimation(name: String, colors: [String: UIColor]? = nil) { + self.currentParams = (name, colors) if let url = getAppBundle().url(forResource: name, withExtension: "json"), let composition = LOTComposition(filePath: url.path) { self.didPlay = false self.animationView()?.sceneModel = composition diff --git a/submodules/Display/Source/ListView.swift b/submodules/Display/Source/ListView.swift index c462c472ba..9a4ab417cb 100644 --- a/submodules/Display/Source/ListView.swift +++ b/submodules/Display/Source/ListView.swift @@ -310,6 +310,13 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture private final var actionsForVSync: [() -> ()] = [] private final var inVSync = false + private var tapGestureRecognizer: UITapGestureRecognizer? + public final var tapped: (() -> Void)? { + didSet { + self.tapGestureRecognizer?.isEnabled = self.tapped != nil + } + } + private let frictionSlider = UISlider() private let springSlider = UISlider() private let freeResistanceSlider = UISlider() @@ -386,7 +393,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.view.addSubview(self.scroller) self.scroller.panGestureRecognizer.cancelsTouchesInView = true self.view.addGestureRecognizer(self.scroller.panGestureRecognizer) - + let trackingRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.trackingGesture(_:))) trackingRecognizer.delegate = self trackingRecognizer.cancelsTouchesInView = false @@ -420,6 +427,12 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self?.updateReordering(offset: offset) })) + let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + tapGestureRecognizer.isEnabled = false + tapGestureRecognizer.delegate = self + self.view.addGestureRecognizer(tapGestureRecognizer) + self.tapGestureRecognizer = tapGestureRecognizer + self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent)) self.displayLink.add(to: RunLoop.main, forMode: RunLoop.Mode.common) @@ -449,6 +462,10 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture self.reorderFeedbackDisposable?.dispose() } + @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { + self.tapped?() + } + private func displayLinkEvent() { self.updateAnimations() } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index fb19d71752..2805cc4b13 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -13460,6 +13460,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return nil } } + controller.dismissed = { [weak self] in + if let strongSelf = self { + strongSelf.chatDisplayNode.historyNode.tapped = nil + } + } + strongSelf.chatDisplayNode.historyNode.tapped = { [weak controller] in + controller?.dimTapped() + } strongSelf.present(controller, in: .window(.root)) strongSelf.themeSceen = controller }) diff --git a/submodules/TelegramUI/Sources/ChatThemeScreen.swift b/submodules/TelegramUI/Sources/ChatThemeScreen.swift index 81255beb9a..0d99e5773c 100644 --- a/submodules/TelegramUI/Sources/ChatThemeScreen.swift +++ b/submodules/TelegramUI/Sources/ChatThemeScreen.swift @@ -529,6 +529,8 @@ final class ChatThemeScreen: ViewController { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + var dismissed: (() -> Void)? + var passthroughHitTestImpl: ((CGPoint) -> UIView?)? { didSet { if self.isNodeLoaded { @@ -572,7 +574,7 @@ final class ChatThemeScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = ChatThemeScreenNode(context: self.context, presentationData: self.presentationData, animatedEmojiStickers: self.animatedEmojiStickers, initiallySelectedEmoticon: self.initiallySelectedEmoticon, dismissByTapOutside: self.dismissByTapOutside) + self.displayNode = ChatThemeScreenNode(context: self.context, presentationData: self.presentationData, controller: self, animatedEmojiStickers: self.animatedEmojiStickers, initiallySelectedEmoticon: self.initiallySelectedEmoticon, dismissByTapOutside: self.dismissByTapOutside) self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl self.controllerNode.previewTheme = { [weak self] emoticon, dark in guard let strongSelf = self else { @@ -629,6 +631,8 @@ final class ChatThemeScreen: ViewController { }) self.controllerNode.animateOut(completion: completion) + + self.dismissed?() } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -636,6 +640,10 @@ final class ChatThemeScreen: ViewController { self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } + + func dimTapped() { + self.controllerNode.dimTapped() + } } private func iconColors(theme: PresentationTheme) -> [String: UIColor] { @@ -658,6 +666,7 @@ private func iconColors(theme: PresentationTheme) -> [String: UIColor] { private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData + private weak var controller: ChatThemeScreen? private let dismissByTapOutside: Bool private let dimNode: ASDisplayNode @@ -671,7 +680,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega private let textNode: ImmediateTextNode private let cancelButton: HighlightableButtonNode private let switchThemeButton: HighlightTrackingButtonNode - private let animationNode: AnimationNode + private let animationContainerNode: ASDisplayNode + private var animationNode: AnimationNode private let doneButton: SolidRoundedButtonNode private let listNode: ListView @@ -704,8 +714,9 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega var dismiss: (() -> Void)? var cancel: (() -> Void)? - init(context: AccountContext, presentationData: PresentationData, animatedEmojiStickers: [String: [StickerPackItem]], initiallySelectedEmoticon: String?, dismissByTapOutside: Bool) { + init(context: AccountContext, presentationData: PresentationData, controller: ChatThemeScreen, animatedEmojiStickers: [String: [StickerPackItem]], initiallySelectedEmoticon: String?, dismissByTapOutside: Bool) { self.context = context + self.controller = controller self.initiallySelectedEmoticon = initiallySelectedEmoticon self.selectedEmoticon = initiallySelectedEmoticon self.selectedEmoticonPromise = ValuePromise(initiallySelectedEmoticon) @@ -754,6 +765,9 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) self.switchThemeButton = HighlightTrackingButtonNode() + self.animationContainerNode = ASDisplayNode() + self.animationContainerNode.isUserInteractionEnabled = false + self.animationNode = AnimationNode(animation: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme), scale: 1.0) self.animationNode.isUserInteractionEnabled = false @@ -768,7 +782,6 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega self.backgroundColor = nil self.isOpaque = false - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) self.addSubnode(self.dimNode) self.wrappingScrollNode.view.delegate = self @@ -784,7 +797,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega self.contentContainerNode.addSubnode(self.textNode) self.contentContainerNode.addSubnode(self.doneButton) - self.topContentContainerNode.addSubnode(self.animationNode) + self.topContentContainerNode.addSubnode(self.animationContainerNode) + self.animationContainerNode.addSubnode(self.animationNode) self.topContentContainerNode.addSubnode(self.switchThemeButton) self.topContentContainerNode.addSubnode(self.listNode) self.topContentContainerNode.addSubnode(self.cancelButton) @@ -820,7 +834,7 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega let action: (String?) -> Void = { [weak self] emoticon in if let strongSelf = self, strongSelf.selectedEmoticon != emoticon { - strongSelf.animateCrossfade(animateBackground: strongSelf.presentationData.theme.overallDarkAppearance, updateSunIcon: true) + strongSelf.animateCrossfade(animateIcon: false) strongSelf.previewTheme?(emoticon, strongSelf.isDarkAppearance) strongSelf.selectedEmoticon = emoticon @@ -928,11 +942,6 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega let previousTheme = self.presentationData.theme self.presentationData = presentationData - if let effectView = self.effectNode.view as? UIVisualEffectView { - effectView.effect = UIBlurEffect(style: presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) - } - - self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { @@ -943,7 +952,19 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme)) if self.animationNode.isPlaying { - + if let animationNode = self.animationNode.makeCopy(colors: iconColors(theme: self.presentationData.theme), progress: 0.25) { + let previousAnimationNode = self.animationNode + self.animationNode = animationNode + + animationNode.completion = { [weak previousAnimationNode] in + previousAnimationNode?.removeFromSupernode() + } + animationNode.isUserInteractionEnabled = false + animationNode.frame = previousAnimationNode.frame + previousAnimationNode.supernode?.insertSubnode(animationNode, belowSubnode: previousAnimationNode) + previousAnimationNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + animationNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } } else { self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) } @@ -963,8 +984,21 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega self.cancel?() } + func dimTapped() { + if self.selectedEmoticon == self.initiallySelectedEmoticon { + self.cancelButtonPressed() + } else { + let alertController = textAlertController(context: self.context, updatedPresentationData: (self.presentationData, .single(self.presentationData)), title: nil, text: self.presentationData.strings.Conversation_Theme_DismissAlert, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Conversation_Theme_DismissAlertApply, action: { [weak self] in + if let strongSelf = self { + strongSelf.completion?(strongSelf.selectedEmoticon) + } + })], actionLayout: .horizontal, dismissOnOutsideTap: true) + self.present?(alertController) + } + } + @objc func switchThemePressed() { - self.animateCrossfade(animateBackground: true) + self.animateCrossfade(animateIcon: false) self.animationNode.setAnimation(name: self.isDarkAppearance ? "anim_sun_reverse" : "anim_sun", colors: iconColors(theme: self.presentationData.theme)) self.animationNode.playOnce() @@ -975,16 +1009,10 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega let _ = ApplicationSpecificNotice.incrementChatSpecificThemesDarkPreviewTip(accountManager: self.context.sharedContext.accountManager, count: 3).start() } - @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if self.dismissByTapOutside, case .ended = recognizer.state { - self.cancelButtonPressed() - } - } - - private func animateCrossfade(animateBackground: Bool = true, updateSunIcon: Bool = false) { + private func animateCrossfade(animateIcon: Bool = true) { let delay: Double = 0.2 - if let snapshotView = self.animationNode.view.snapshotView(afterScreenUpdates: false) { + if animateIcon, let snapshotView = self.animationNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = self.animationNode.frame self.animationNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.animationNode.view) @@ -993,15 +1021,19 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega }) } - if animateBackground, let snapshotView = self.backgroundNode.view.snapshotView(afterScreenUpdates: false) { - snapshotView.frame = self.backgroundNode.frame - self.backgroundNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.backgroundNode.view) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: delay, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) + Queue.mainQueue().after(delay) { + if let effectView = self.effectNode.view as? UIVisualEffectView { + UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveEaseInOut) { + effectView.effect = UIBlurEffect(style: self.presentationData.theme.actionSheet.backgroundType == .light ? .light : .dark) + } completion: { _ in + } + } + + let previousColor = self.contentBackgroundNode.backgroundColor ?? .clear + self.contentBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.itemBackgroundColor + self.contentBackgroundNode.layer.animate(from: previousColor.cgColor, to: (self.contentBackgroundNode.backgroundColor ?? .clear).cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.3) } - + if let snapshotView = self.contentContainerNode.view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = self.contentContainerNode.frame self.contentContainerNode.view.superview?.insertSubview(snapshotView, aboveSubview: self.contentContainerNode.view) @@ -1055,33 +1087,26 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega func animateOut(completion: (() -> Void)? = nil) { self.animatedOut = true - var dimCompleted = false - var offsetCompleted = false - - let internalCompletion: () -> Void = { [weak self] in - if let strongSelf = self, dimCompleted && offsetCompleted { - strongSelf.dismiss?() - } - completion?() - } - - self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in - dimCompleted = true - internalCompletion() - }) - let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY - let dimPosition = self.dimNode.layer.position - self.dimNode.layer.animatePosition(from: dimPosition, to: CGPoint(x: dimPosition.x, y: dimPosition.y - offset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) - self.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in - offsetCompleted = true - internalCompletion() + self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.dismiss?() + completion?() + } }) } var passthroughHitTestImpl: ((CGPoint) -> UIView?)? override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.bounds.contains(point) { + var presentingAlertController = false + self.controller?.forEachController({ c in + if c is AlertController { + presentingAlertController = true + } + return true + }) + + if !presentingAlertController && self.bounds.contains(point) { if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { if let result = self.passthroughHitTestImpl?(point) { return result @@ -1136,7 +1161,8 @@ private class ChatThemeScreenNode: ViewControllerTracingNode, UIScrollViewDelega let switchThemeSize = CGSize(width: 44.0, height: 44.0) let switchThemeFrame = CGRect(origin: CGPoint(x: 3.0, y: 6.0), size: switchThemeSize) transition.updateFrame(node: self.switchThemeButton, frame: switchThemeFrame) - transition.updateFrame(node: self.animationNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0)) + transition.updateFrame(node: self.animationContainerNode, frame: switchThemeFrame.insetBy(dx: 9.0, dy: 9.0)) + transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(), size: self.animationContainerNode.frame.size)) let cancelSize = CGSize(width: 44.0, height: 44.0) let cancelFrame = CGRect(origin: CGPoint(x: contentFrame.width - cancelSize.width - 3.0, y: 6.0), size: cancelSize)