diff --git a/submodules/AttachmentUI/BUILD b/submodules/AttachmentUI/BUILD index f5b342e9ed..a4e4997ee3 100644 --- a/submodules/AttachmentUI/BUILD +++ b/submodules/AttachmentUI/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/ContextUI:ContextUI", "//submodules/ManagedAnimationNode:ManagedAnimationNode", "//submodules/PhotoResources:PhotoResources", + "//submodules/Components/AnimatedStickerComponent:AnimatedStickerComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/AttachmentUI/Sources/AttachmentContainer.swift b/submodules/AttachmentUI/Sources/AttachmentContainer.swift index 6d6b52f637..3b7faef988 100644 --- a/submodules/AttachmentUI/Sources/AttachmentContainer.swift +++ b/submodules/AttachmentUI/Sources/AttachmentContainer.swift @@ -63,6 +63,8 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate { private var panGestureRecognizer: UIPanGestureRecognizer? + var isPanningUpdated: (Bool) -> Void = { _ in } + override init() { self.wrappingNode = ASDisplayNode() self.clipNode = ASDisplayNode() @@ -175,7 +177,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate { } else { topInset = edgeTopInset } - + self.panGestureArguments = (topInset, 0.0, scrollView, listNode) case .changed: guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else { @@ -260,6 +262,13 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate { let offset = currentTopInset + panOffset let topInset: CGFloat = edgeTopInset + let completion = { + guard self.panGestureArguments == nil else { + return + } + self.isPanningUpdated(false) + } + var dismissing = false if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { self.interactivelyDismissed?() @@ -277,10 +286,14 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate { let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: transition) + + Queue.mainQueue().after(0.5, completion) } else { self.isExpanded = true self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.3, curve: .easeInOut)) + + Queue.mainQueue().after(0.35, completion) } } else if (velocity.y < -300.0 || offset < topInset / 2.0) { if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode { @@ -294,6 +307,8 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate { self.isExpanded = true self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: transition) + + Queue.mainQueue().after(0.5, completion) } else { if let listNode = listNode { listNode.scroller.setContentOffset(CGPoint(), animated: false) @@ -305,6 +320,8 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate { } self.update(layout: layout, controllers: controllers, coveredByModalTransition: coveredByModalTransition, transition: .animated(duration: 0.3, curve: .easeInOut)) + + Queue.mainQueue().after(0.35, completion) } if !dismissing { diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 1cc2a9e8a6..74f05cc824 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -28,6 +28,8 @@ public protocol AttachmentContainable: ViewController { var cancelPanGesture: () -> Void { get set } var isContainerPanning: () -> Bool { get set } + func isContainerPanningUpdated(_ panning: Bool) + func resetForReuse() func prepareForReuse() @@ -35,6 +37,10 @@ public protocol AttachmentContainable: ViewController { } public extension AttachmentContainable { + func isContainerPanningUpdated(_ panning: Bool) { + + } + func resetForReuse() { } @@ -209,6 +215,12 @@ public class AttachmentController: ViewController { } } + self.container.isPanningUpdated = { [weak self] value in + if let strongSelf = self, let currentController = strongSelf.currentControllers.last, !value { + currentController.isContainerPanningUpdated(value) + } + } + self.panel.selectionChanged = { [weak self] type in if let strongSelf = self { return strongSelf.switchToController(type) diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index d5129fd715..0d61499530 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -13,6 +13,7 @@ import ChatPresentationInterfaceState import ChatSendMessageActionUI import ChatTextLinkEditUI import PhotoResources +import AnimatedStickerComponent private let buttonSize = CGSize(width: 88.0, height: 49.0) private let smallButtonWidth: CGFloat = 69.0 @@ -23,12 +24,14 @@ private final class IconComponent: Component { public let account: Account public let name: String public let file: TelegramMediaFile? + public let animationName: String? public let tintColor: UIColor? - public init(account: Account, name: String, file: TelegramMediaFile?, tintColor: UIColor?) { + public init(account: Account, name: String, file: TelegramMediaFile?, animationName: String?, tintColor: UIColor?) { self.account = account self.name = name self.file = file + self.animationName = animationName self.tintColor = tintColor } @@ -42,6 +45,9 @@ private final class IconComponent: Component { if lhs.file?.fileId != rhs.file?.fileId { return false } + if lhs.animationName != rhs.animationName { + return false + } if lhs.tintColor != rhs.tintColor { return false } @@ -72,7 +78,6 @@ private final class IconComponent: Component { self.image = nil } - let _ = freeMediaFileInteractiveFetched(account: component.account, fileReference: .standalone(media: file)).start() self.disposable = (svgIconImageFile(account: component.account, fileReference: .standalone(media: file), fetched: true) |> runOn(Queue.concurrentDefaultQueue()) |> deliverOnMainQueue).start(next: { [weak self] transform in @@ -154,6 +159,7 @@ private final class AttachButtonComponent: CombinedComponent { static var body: Body { let icon = Child(IconComponent.self) + let animatedIcon = Child(AnimatedStickerComponent.self) let title = Child(Text.self) let button = Child(Rectangle.self) @@ -190,20 +196,49 @@ private final class AttachButtonComponent: CombinedComponent { imageName = "" imageFile = nil } + let tintColor = component.isSelected ? component.theme.rootController.tabBar.selectedIconColor : component.theme.rootController.tabBar.iconColor let iconSize = CGSize(width: 30.0, height: 30.0) - let icon = icon.update( - component: IconComponent( - account: component.context.account, - name: imageName, - file: imageFile, - tintColor: tintColor - ), - availableSize: iconSize, - transition: context.transition - ) + let topInset: CGFloat = 4.0 + UIScreenPixel + let spacing: CGFloat = 15.0 + UIScreenPixel + + let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - iconSize.width) / 2.0), y: topInset), size: iconSize) + if let imageFile = imageFile, (imageFile.fileName ?? "").lowercased().hasSuffix(".tgs") { + let icon = animatedIcon.update( + component: AnimatedStickerComponent( + account: component.context.account, + animation: AnimatedStickerComponent.Animation( + source: .file(media: imageFile), + loop: false, + tintColor: tintColor + ), + isAnimating: component.isSelected, + size: CGSize(width: iconSize.width, height: iconSize.height) + ), + availableSize: iconSize, + transition: context.transition + ) + context.add(icon + .position(CGPoint(x: iconFrame.midX, y: iconFrame.midY)) + ) + } else { + let icon = icon.update( + component: IconComponent( + account: component.context.account, + name: imageName, + file: imageFile, + animationName: nil, + tintColor: tintColor + ), + availableSize: iconSize, + transition: context.transition + ) + context.add(icon + .position(CGPoint(x: iconFrame.midX, y: iconFrame.midY)) + ) + } let title = title.update( component: Text( @@ -225,19 +260,11 @@ private final class AttachButtonComponent: CombinedComponent { transition: .immediate ) - let topInset: CGFloat = 4.0 + UIScreenPixel - let spacing: CGFloat = 15.0 + UIScreenPixel - - let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - icon.size.width) / 2.0), y: topInset), size: iconSize) let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - title.size.width) / 2.0), y: iconFrame.midY + spacing), size: title.size) context.add(title .position(CGPoint(x: titleFrame.midX, y: titleFrame.midY)) ) - - context.add(icon - .position(CGPoint(x: iconFrame.midX, y: iconFrame.midY)) - ) context.add(button .position(CGPoint(x: context.availableSize.width / 2.0, y: context.availableSize.height / 2.0)) @@ -256,6 +283,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { private var presentationData: PresentationData private var presentationDataDisposable: Disposable? + private var iconDisposables: [MediaId: Disposable] = [:] + private var presentationInterfaceState: ChatPresentationInterfaceState private var interfaceInteraction: ChatPanelInterfaceInteraction? @@ -492,6 +521,9 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { deinit { self.presentationDataDisposable?.dispose() + for (_, disposable) in self.iconDisposables { + disposable.dispose() + } } override func didLoad() { @@ -578,6 +610,11 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { } let type = self.buttons[i] + if case let .app(_, _, iconFile) = type { + if self.iconDisposables[iconFile.fileId] == nil { + self.iconDisposables[iconFile.fileId] = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: iconFile)).start() + } + } let _ = buttonView.update( transition: buttonTransition, component: AnyComponent(AttachButtonComponent( diff --git a/submodules/Components/AnimatedStickerComponent/BUILD b/submodules/Components/AnimatedStickerComponent/BUILD index a480472de6..939314aa4e 100644 --- a/submodules/Components/AnimatedStickerComponent/BUILD +++ b/submodules/Components/AnimatedStickerComponent/BUILD @@ -14,7 +14,7 @@ swift_library( "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/Components/HierarchyTrackingLayer:HierarchyTrackingLayer", - + "//submodules/TelegramCore:TelegramCore", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/AnimatedStickerComponent/Sources/AnimatedStickerComponent.swift b/submodules/Components/AnimatedStickerComponent/Sources/AnimatedStickerComponent.swift index f7a1248815..d9f95299f1 100644 --- a/submodules/Components/AnimatedStickerComponent/Sources/AnimatedStickerComponent.swift +++ b/submodules/Components/AnimatedStickerComponent/Sources/AnimatedStickerComponent.swift @@ -4,32 +4,48 @@ import ComponentFlow import AnimatedStickerNode import TelegramAnimatedStickerNode import HierarchyTrackingLayer +import TelegramCore public final class AnimatedStickerComponent: Component { public struct Animation: Equatable { - public var name: String - public var loop: Bool - public var isAnimating: Bool + public enum Source: Equatable { + case bundle(name: String) + case file(media: TelegramMediaFile) + } - public init(name: String, loop: Bool, isAnimating: Bool = true) { - self.name = name + public var source: Source + public var loop: Bool + public var tintColor: UIColor? + + public init(source: Source, loop: Bool, tintColor: UIColor? = nil) { + self.source = source self.loop = loop - self.isAnimating = isAnimating + self.tintColor = tintColor } } + public let account: Account public let animation: Animation + public let isAnimating: Bool public let size: CGSize - public init(animation: Animation, size: CGSize) { + public init(account: Account, animation: Animation, isAnimating: Bool = true, size: CGSize) { + self.account = account self.animation = animation + self.isAnimating = isAnimating self.size = size } public static func ==(lhs: AnimatedStickerComponent, rhs: AnimatedStickerComponent) -> Bool { + if lhs.account !== rhs.account { + return false + } if lhs.animation != rhs.animation { return false } + if lhs.isAnimating != rhs.isAnimating { + return false + } if lhs.size != rhs.size { return false } @@ -72,20 +88,40 @@ public final class AnimatedStickerComponent: Component { func update(component: AnimatedStickerComponent, availableSize: CGSize, transition: Transition) -> CGSize { if self.component?.animation != component.animation { - self.component = component - self.animationNode?.view.removeFromSuperview() let animationNode = AnimatedStickerNode() - animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: component.animation.name), width: Int(component.size.width * 2.0), height: Int(component.size.height * 2.0), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) + let source: AnimatedStickerNodeSource + switch component.animation.source { + case let .bundle(name): + source = AnimatedStickerNodeLocalFileSource(name: name) + case let .file(media): + source = AnimatedStickerResourceSource(account: component.account, resource: media.resource, fitzModifier: nil, isVideo: false) + } + animationNode.setOverlayColor(component.animation.tintColor, replace: true, animated: false) + + var playbackMode: AnimatedStickerPlaybackMode = .still(.start) + if component.animation.loop { + playbackMode = .loop + } else if component.isAnimating { + playbackMode = .once + } + animationNode.setup(source: source, width: Int(component.size.width * 2.0), height: Int(component.size.height * 2.0), playbackMode: playbackMode, mode: .direct(cachePathPrefix: nil)) animationNode.visibility = self.isInHierarchy self.animationNode = animationNode self.addSubnode(animationNode) } - let animationSize = component.size + if !component.animation.loop && component.isAnimating != self.component?.isAnimating { + if component.isAnimating { + let _ = self.animationNode?.playIfNeeded() + } + } + self.component = component + + let animationSize = component.size let size = CGSize(width: min(animationSize.width, availableSize.width), height: min(animationSize.height, availableSize.height)) if let animationNode = self.animationNode { diff --git a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift index fb7f2ff311..0125c78dcc 100644 --- a/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift +++ b/submodules/PeerInfoUI/CreateExternalMediaStreamScreen/Sources/CreateExternalMediaStreamScreen.swift @@ -203,8 +203,9 @@ private final class CreateExternalMediaStreamScreenComponent: CombinedComponent let animation = animation.update( component: AnimatedStickerComponent( + account: state.context.account, animation: AnimatedStickerComponent.Animation( - name: "CreateStream", + source: .bundle(name: "CreateStream"), loop: true ), size: CGSize(width: 138.0, height: 138.0) diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 2ad71c35b0..dba852a080 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1091,7 +1091,7 @@ public class Account { self.managedOperationsDisposable.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedApplyPendingScheduledMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedSynchronizeAvailableReactions(postbox: self.postbox, network: self.network).start()) - self.managedOperationsDisposable.add(managedSynchronizeAttachMenuBots(postbox: self.postbox, network: self.network).start()) + self.managedOperationsDisposable.add(managedSynchronizeAttachMenuBots(postbox: self.postbox, network: self.network, force: true).start()) self.managedOperationsDisposable.add(managedSynchronizeNotificationSoundList(postbox: self.postbox, network: self.network).start()) if !supplementary { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift index 78ab97ebb1..3dd1f7d29f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AttachMenuBots.swift @@ -3,7 +3,6 @@ import TelegramApi import Postbox import SwiftSignalKit - public final class AttachMenuBots: Equatable, Codable { public final class Bot: Equatable, Codable { private enum CodingKeys: String, CodingKey { @@ -188,11 +187,11 @@ private func setCachedAttachMenuBots(transaction: Transaction, attachMenuBots: A } } -func managedSynchronizeAttachMenuBots(postbox: Postbox, network: Network) -> Signal { +func managedSynchronizeAttachMenuBots(postbox: Postbox, network: Network, force: Bool = false) -> Signal { let poll = Signal { subscriber in let signal: Signal = cachedAttachMenuBots(postbox: postbox) |> mapToSignal { current in - return (network.request(Api.functions.messages.getAttachMenuBots(hash: current?.hash ?? 0)) + return (network.request(Api.functions.messages.getAttachMenuBots(hash: force ? 0 : (current?.hash ?? 0))) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 7c6050ec48..2b910e38e7 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -209,10 +209,7 @@ public final class WebAppController: ViewController, AttachmentContainable { let placeholderNode = ShimmerEffectNode() self.addSubnode(placeholderNode) self.placeholderNode = placeholderNode - - if controller.buttonText == nil { - self.addSubnode(self.loadingProgressNode) - } + self.addSubnode(self.loadingProgressNode) if let iconFile = controller.iconFile { let _ = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: iconFile)).start() @@ -337,6 +334,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) } + private var validLayout: (ContainerViewLayout, CGFloat)? func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { let previous = self.validLayout?.0 @@ -344,7 +342,6 @@ public final class WebAppController: ViewController, AttachmentContainable { if let webView = self.webView, let controller = self.controller { let frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom - layout.additionalInsets.bottom))) - webView.updateFrame(frame: frame, panning: controller.isContainerPanning(), transition: transition) } @@ -371,6 +368,17 @@ public final class WebAppController: ViewController, AttachmentContainable { } } + func isContainerPanningUpdated(_ panning: Bool) { + guard let (layout, navigationBarHeight) = self.validLayout else { + return + } + + if let webView = self.webView { + let frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom - layout.additionalInsets.bottom))) + webView.updateFrame(frame: frame, panning: panning, transition: .immediate) + } + } + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "estimatedProgress", let webView = self.webView { self.loadingProgressNode.updateProgress(webView.estimatedProgress, animated: true)