From 7ef63a81df3841bbc595510531c772e3e49ea2f2 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Fri, 10 May 2024 20:57:12 +0400 Subject: [PATCH] [WIP] Message effects --- Tests/LottieMetalMacTest/.gitignore | 2 + Tests/LottieMetalMacTest/BUILD | 173 ++++ .../MacSources/AppDelegate.swift | 19 + .../MacSources/ViewController.swift | 11 + .../Resources/Main.storyboard | 697 ++++++++++++++++ .../MacSources/ViewController.swift | 0 .../Sources/CompareToReferenceRendering.swift | 2 + .../Sources/ViewController.swift | 17 +- .../Sources/AnimatedStickerNode.swift | 2 +- submodules/AttachmentUI/BUILD | 2 + .../Sources/AttachmentPanel.swift | 32 +- .../Sources/ChatMessageBackground.swift | 2 +- submodules/ChatSendMessageActionUI/BUILD | 3 + ...ChatSendMessageActionSheetController.swift | 10 +- ...SendMessageActionSheetControllerNode.swift | 2 +- .../ChatSendMessageContextScreen.swift | 371 ++++++--- .../Sources/MessageItemView.swift | 121 ++- .../Sources/SendButton.swift | 134 ++++ submodules/Display/Source/PortalView.swift | 6 + .../Sources/DrawingReactionView.swift | 2 +- .../ReactionContextBackgroundNode.swift | 4 + .../Sources/ReactionContextNode.swift | 603 +++++++++----- .../Sources/ReactionSelectionNode.swift | 11 +- .../State/AvailableMessageEffects.swift | 8 +- .../Sources/ChatInputTextNode.swift | 43 + .../Sources/ChatMessageBubbleItemNode.swift | 57 +- .../ChatMessageSelectionInputPanelNode.swift | 2 +- .../Sources/ChatShareMessageTagView.swift | 2 +- .../Components/EntityKeyboard/BUILD | 1 + .../Sources/EmojiPagerContentComponent.swift | 83 +- .../Sources/EmojiPagerContentSignals.swift | 21 +- .../Sources/InlineFileIconLayer.swift | 375 +++++++++ .../TelegramUI/Components/LottieCpp/BUILD | 16 +- .../LottieCpp/LottieAnimationContainer.h | 28 + .../Public/Keyframes/ValueInterpolators.cpp | 17 +- .../Public/Keyframes/ValueInterpolators.hpp | 9 - .../Sources/LottieAnimationContainer.mm | 43 + .../LottieMetalAnimatedStickerNode.swift | 743 +++++++++++------- .../Sources/RenderTreeSerialization.swift | 8 +- .../StoryItemSetContainerComponent.swift | 2 +- ...ChatControllerOpenMessageContextMenu.swift | 4 +- ...ChatMessageDisplaySendMessageOptions.swift | 17 +- .../Sources/WallpaperBackgroundNode.swift | 49 +- 43 files changed, 3050 insertions(+), 704 deletions(-) create mode 100644 Tests/LottieMetalMacTest/.gitignore create mode 100644 Tests/LottieMetalMacTest/BUILD create mode 100644 Tests/LottieMetalMacTest/MacSources/AppDelegate.swift create mode 100644 Tests/LottieMetalMacTest/MacSources/ViewController.swift create mode 100644 Tests/LottieMetalMacTest/Resources/Main.storyboard create mode 100644 Tests/LottieMetalTest/MacSources/ViewController.swift create mode 100644 submodules/ChatSendMessageActionUI/Sources/SendButton.swift create mode 100644 submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift diff --git a/Tests/LottieMetalMacTest/.gitignore b/Tests/LottieMetalMacTest/.gitignore new file mode 100644 index 0000000000..31360466c1 --- /dev/null +++ b/Tests/LottieMetalMacTest/.gitignore @@ -0,0 +1,2 @@ +TestData/*.json + diff --git a/Tests/LottieMetalMacTest/BUILD b/Tests/LottieMetalMacTest/BUILD new file mode 100644 index 0000000000..110af1aa5a --- /dev/null +++ b/Tests/LottieMetalMacTest/BUILD @@ -0,0 +1,173 @@ +load("@build_bazel_rules_apple//apple:macos.bzl", + "macos_application", +) + +load("@build_bazel_rules_swift//swift:swift.bzl", + "swift_library", +) + +load("//build-system/bazel-utils:plist_fragment.bzl", + "plist_fragment", +) + +load( + "@build_bazel_rules_apple//apple:resources.bzl", + "apple_resource_bundle", + "apple_resource_group", +) + +load( + "@rules_xcodeproj//xcodeproj:defs.bzl", + "top_level_target", + "top_level_targets", + "xcodeproj", + "xcode_provisioning_profile", +) + +load("@build_bazel_rules_apple//apple:apple.bzl", "local_provisioning_profile") + +load( + "@build_configuration//:variables.bzl", + "telegram_bazel_path", +) + +filegroup( + name = "AppResources", + srcs = glob([ + "Resources/**/*", + ], exclude = ["Resources/**/.*"]), +) + +plist_fragment( + name = "BuildNumberInfoPlist", + extension = "plist", + template = + """ + CFBundleVersion + 1 + """ +) + +plist_fragment( + name = "VersionInfoPlist", + extension = "plist", + template = + """ + CFBundleShortVersionString + 1.0 + """ +) + +plist_fragment( + name = "AppNameInfoPlist", + extension = "plist", + template = + """ + CFBundleDisplayName + Test + """ +) + +plist_fragment( + name = "MacAppInfoPlist", + extension = "plist", + template = + """ + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Telegram + CFBundlePackageType + APPL + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + """ +) + +filegroup( + name = "TestDataBundleFiles", + srcs = glob([ + "TestData/*.json", + ]), + visibility = ["//visibility:public"], +) + +plist_fragment( + name = "TestDataBundleInfoPlist", + extension = "plist", + template = + """ + CFBundleIdentifier + org.telegram.TestDataBundle + CFBundleDevelopmentRegion + en + CFBundleName + TestDataBundle + """ +) + +apple_resource_bundle( + name = "TestDataBundle", + infoplists = [ + ":TestDataBundleInfoPlist", + ], + resources = [ + ":TestDataBundleFiles", + ], +) + +swift_library( + name = "MacLib", + srcs = glob([ + "MacSources/**/*.swift", + ]), + data = [ + "Resources/Main.storyboard", + ], +) + +macos_application( + name = "LottieMetalMacTest", + app_icons = [], + bundle_id = "com.example.hello-world-swift", + infoplists = [ + ":MacAppInfoPlist", + ":BuildNumberInfoPlist", + ":VersionInfoPlist", + ], + minimum_os_version = "10.13", + deps = [ + ":MacLib" + ], + visibility = ["//visibility:public"], +) + +xcodeproj( + name = "LottieMetalMacTest_xcodeproj", + build_mode = "bazel", + bazel_path = telegram_bazel_path, + project_name = "LottieMetalMacTest", + tags = ["manual"], + top_level_targets = top_level_targets( + labels = [ + ":LottieMetalMacTest", + ], + ), + xcode_configurations = { + "Debug": { + "//command_line_option:compilation_mode": "dbg", + }, + "Release": { + "//command_line_option:compilation_mode": "opt", + }, + }, + default_xcode_configuration = "Debug" +) diff --git a/Tests/LottieMetalMacTest/MacSources/AppDelegate.swift b/Tests/LottieMetalMacTest/MacSources/AppDelegate.swift new file mode 100644 index 0000000000..377719dffc --- /dev/null +++ b/Tests/LottieMetalMacTest/MacSources/AppDelegate.swift @@ -0,0 +1,19 @@ +// Copyright 2017 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Cocoa + +@NSApplicationMain +@objc(AppDelegate) +class AppDelegate: NSObject, NSApplicationDelegate {} diff --git a/Tests/LottieMetalMacTest/MacSources/ViewController.swift b/Tests/LottieMetalMacTest/MacSources/ViewController.swift new file mode 100644 index 0000000000..9811f38752 --- /dev/null +++ b/Tests/LottieMetalMacTest/MacSources/ViewController.swift @@ -0,0 +1,11 @@ +import Cocoa + +@objc(ViewController) +class ViewController: NSViewController { + override func viewDidLoad() { + super.viewDidLoad() + + self.view.layer?.backgroundColor = NSColor.blue.cgColor + } +} + diff --git a/Tests/LottieMetalMacTest/Resources/Main.storyboard b/Tests/LottieMetalMacTest/Resources/Main.storyboard new file mode 100644 index 0000000000..d9938c1396 --- /dev/null +++ b/Tests/LottieMetalMacTest/Resources/Main.storyboard @@ -0,0 +1,697 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/LottieMetalTest/MacSources/ViewController.swift b/Tests/LottieMetalTest/MacSources/ViewController.swift new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift b/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift index f80c8cc1a5..b5ef73185c 100644 --- a/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift +++ b/Tests/LottieMetalTest/Sources/CompareToReferenceRendering.swift @@ -161,6 +161,7 @@ func buildAnimationFolderItems(basePath: String, path: String) -> [(String, Stri return result } +@available (iOS 13.0, *) private func processAnimationFolderItems(items: [(String, String)], countPerBucket: Int, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool { let bucketCount = items.count / countPerBucket var buckets: [[(String, String)]] = [] @@ -253,6 +254,7 @@ private func processAnimationFolderItemsParallel(items: [(String, String)], stop return result } +@available (iOS 13.0, *) func processAnimationFolderAsync(basePath: String, path: String, stopOnFailure: Bool, process: @escaping (String, String, Bool) async -> Bool) async -> Bool { let items = buildAnimationFolderItems(basePath: basePath, path: path) return await processAnimationFolderItems(items: items, countPerBucket: 1, stopOnFailure: stopOnFailure, process: process) diff --git a/Tests/LottieMetalTest/Sources/ViewController.swift b/Tests/LottieMetalTest/Sources/ViewController.swift index 78cf3fa90d..c3ed1fc5db 100644 --- a/Tests/LottieMetalTest/Sources/ViewController.swift +++ b/Tests/LottieMetalTest/Sources/ViewController.swift @@ -110,6 +110,8 @@ public final class ViewController: UIViewController { override public func viewDidLoad() { super.viewDidLoad() + SharedDisplayLinkDriver.shared.updateForegroundState(true) + let bundlePath = Bundle.main.path(forResource: "TestDataBundle", ofType: "bundle")! let filePath = bundlePath + "/fireworks.json" @@ -117,12 +119,15 @@ public final class ViewController: UIViewController { self.view.layer.addSublayer(MetalEngine.shared.rootLayer) - if "".isEmpty { + if !"".isEmpty { if #available(iOS 13.0, *) { self.test = ReferenceCompareTest(view: self.view) } - } else if !"".isEmpty { - let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath)) + } else if "".isEmpty { + let cachedAnimation = cacheLottieMetalAnimation(path: filePath)! + let animation = parseCachedLottieMetalAnimation(data: cachedAnimation)! + + /*let animationData = try! Data(contentsOf: URL(fileURLWithPath: filePath)) var startTime = CFAbsoluteTimeGetCurrent() let animation = LottieAnimation(data: animationData)! @@ -131,9 +136,9 @@ public final class ViewController: UIViewController { startTime = CFAbsoluteTimeGetCurrent() let animationContainer = LottieAnimationContainer(animation: animation) animationContainer.update(0) - print("Build time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms") + print("Build time: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")*/ - let lottieLayer = LottieContentLayer(animation: animationContainer) + let lottieLayer = LottieContentLayer(content: animation) lottieLayer.frame = CGRect(origin: CGPoint(x: 10.0, y: 50.0), size: CGSize(width: 256.0, height: 256.0)) self.view.layer.addSublayer(lottieLayer) lottieLayer.setNeedsUpdate() @@ -162,7 +167,7 @@ public final class ViewController: UIViewController { var frameIndex = 0 while true { animationContainer.update(frameIndex) - let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false) + //let _ = animationRenderer.render(for: CGSize(width: CGFloat(performanceFrameSize), height: CGFloat(performanceFrameSize)), useReferenceRendering: false) frameIndex = (frameIndex + 1) % animationContainer.animation.frameCount numUpdates += 1 let timestamp = CFAbsoluteTimeGetCurrent() diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index b6b3a76dd3..7e3ccaeb2a 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -168,7 +168,7 @@ public protocol AnimatedStickerNode: ASDisplayNode { var visibility: Bool { get set } var overrideVisibility: Bool { get set } - var isPlayingChanged: (Bool) -> Void { get } + var isPlayingChanged: (Bool) -> Void { get set } func cloneCurrentFrame(from otherNode: AnimatedStickerNode?) func setup(source: AnimatedStickerNodeSource, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode, mode: AnimatedStickerMode) diff --git a/submodules/AttachmentUI/BUILD b/submodules/AttachmentUI/BUILD index 3770639b73..107d258c74 100644 --- a/submodules/AttachmentUI/BUILD +++ b/submodules/AttachmentUI/BUILD @@ -39,6 +39,8 @@ swift_library( "//submodules/TextFormat:TextFormat", "//submodules/TelegramUI/Components/LegacyMessageInputPanel", "//submodules/TelegramUI/Components/LegacyMessageInputPanelInputView", + "//submodules/ReactionSelectionNode", + "//submodules/TelegramUI/Components/Chat/TopMessageReactions", ], visibility = [ "//visibility:public", diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index f848e259ec..f7b59bff1c 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -21,6 +21,8 @@ import ShimmerEffect import TextFormat import LegacyMessageInputPanel import LegacyMessageInputPanelInputView +import ReactionSelectionNode +import TopMessageReactions private let buttonSize = CGSize(width: 88.0, height: 49.0) private let smallButtonWidth: CGFloat = 69.0 @@ -926,9 +928,31 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { if case .media = strongSelf.presentationInterfaceState.inputMode { hasEntityKeyboard = true } - let _ = (strongSelf.context.account.viewTracker.peerView(peerId) - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak self] peerView in + + let effectItems: Signal<[ReactionItem]?, NoError> + if strongSelf.presentationInterfaceState.chatLocation.peerId != strongSelf.context.account.peerId && strongSelf.presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.CloudUser { + effectItems = effectMessageReactions(context: strongSelf.context) + |> map(Optional.init) + } else { + effectItems = .single(nil) + } + + let availableMessageEffects = strongSelf.context.availableMessageEffects |> take(1) + let hasPremium = strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + + let _ = (combineLatest( + strongSelf.context.account.viewTracker.peerView(peerId) |> take(1), + effectItems, + availableMessageEffects, + hasPremium + ) + |> deliverOnMainQueue).startStandalone(next: { [weak self] peerView, effectItems, availableMessageEffects, hasPremium in guard let strongSelf = self, let peer = peerViewMainPeer(peerView) else { return } @@ -955,7 +979,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate { } }, schedule: { [weak textInputPanelNode] _ in textInputPanelNode?.sendMessage(.schedule) - }) + }, reactionItems: effectItems, availableMessageEffects: availableMessageEffects, isPremium: hasPremium) strongSelf.presentInGlobalOverlay(controller) }) }, openScheduledMessages: { diff --git a/submodules/ChatMessageBackground/Sources/ChatMessageBackground.swift b/submodules/ChatMessageBackground/Sources/ChatMessageBackground.swift index 59bd0ac957..be74c8e3b1 100644 --- a/submodules/ChatMessageBackground/Sources/ChatMessageBackground.swift +++ b/submodules/ChatMessageBackground/Sources/ChatMessageBackground.swift @@ -492,7 +492,7 @@ public func bubbleMaskForType(_ type: ChatMessageBackgroundType, graphics: Princ } public final class ChatMessageBubbleBackdrop: ASDisplayNode { - private var backgroundContent: WallpaperBubbleBackgroundNode? + public private(set) var backgroundContent: WallpaperBubbleBackgroundNode? private var currentType: ChatMessageBackgroundType? private var currentMaskMode: Bool? diff --git a/submodules/ChatSendMessageActionUI/BUILD b/submodules/ChatSendMessageActionUI/BUILD index d850ac9f2d..f3008685ce 100644 --- a/submodules/ChatSendMessageActionUI/BUILD +++ b/submodules/ChatSendMessageActionUI/BUILD @@ -33,7 +33,10 @@ swift_library( "//submodules/Components/MultilineTextWithEntitiesComponent", "//submodules/Components/MultilineTextComponent", "//submodules/TelegramUI/Components/LottieMetal", + "//submodules/AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode", + "//submodules/ActivityIndicator", + "//submodules/UndoUI", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift index 4fe1ad49d9..71e76349a5 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetController.swift @@ -210,7 +210,9 @@ public func makeChatSendMessageActionSheetController( completion: @escaping () -> Void, sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void, - reactionItems: [ReactionItem]? = nil + reactionItems: [ReactionItem]? = nil, + availableMessageEffects: AvailableMessageEffects? = nil, + isPremium: Bool = false ) -> ChatSendMessageActionSheetController { if textInputView.text.isEmpty { return ChatSendMessageActionSheetControllerImpl( @@ -229,7 +231,7 @@ public func makeChatSendMessageActionSheetController( completion: completion, sendMessage: sendMessage, schedule: schedule, - reactionItems: reactionItems + reactionItems: nil ) } @@ -250,6 +252,8 @@ public func makeChatSendMessageActionSheetController( completion: completion, sendMessage: sendMessage, schedule: schedule, - reactionItems: reactionItems + reactionItems: reactionItems, + availableMessageEffects: availableMessageEffects, + isPremium: isPremium ) } diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift index 54d26b69a0..e369006a21 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -388,7 +388,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, context: context, animationCache: context.animationCache, presentationData: presentationData, - items: reactionItems.map(ReactionContextItem.reaction), + items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) }, selectedItems: Set(), title: "Add an animated effect", reactionsLocked: false, diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift index af5d79c727..6741d4d617 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageContextScreen.swift @@ -18,6 +18,9 @@ import ReactionSelectionNode import EntityKeyboard import LottieMetal import TelegramAnimatedStickerNode +import AnimatedStickerNode +import ChatInputTextNode +import UndoUI func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect { let sourceWindowFrame = fromView.convert(frame, to: nil) @@ -48,6 +51,8 @@ final class ChatSendMessageContextScreenComponent: Component { let sendMessage: (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void let schedule: (ChatSendMessageActionSheetController.MessageEffect?) -> Void let reactionItems: [ReactionItem]? + let availableMessageEffects: AvailableMessageEffects? + let isPremium: Bool init( context: AccountContext, @@ -65,7 +70,9 @@ final class ChatSendMessageContextScreenComponent: Component { completion: @escaping () -> Void, sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void, - reactionItems: [ReactionItem]? + reactionItems: [ReactionItem]?, + availableMessageEffects: AvailableMessageEffects?, + isPremium: Bool ) { self.context = context self.peerId = peerId @@ -83,6 +90,8 @@ final class ChatSendMessageContextScreenComponent: Component { self.sendMessage = sendMessage self.schedule = schedule self.reactionItems = reactionItems + self.availableMessageEffects = availableMessageEffects + self.isPremium = isPremium } static func ==(lhs: ChatSendMessageContextScreenComponent, rhs: ChatSendMessageContextScreenComponent) -> Bool { @@ -115,7 +124,7 @@ final class ChatSendMessageContextScreenComponent: Component { final class View: UIView { private let backgroundView: BlurredBackgroundView - private var sendButton: HighlightTrackingButton? + private var sendButton: SendButton? private var messageItemView: MessageItemView? private var actionsStackNode: ContextControllerActionsStackNode? private var reactionContextNode: ReactionContextNode? @@ -129,7 +138,10 @@ final class ChatSendMessageContextScreenComponent: Component { private let messageEffectDisposable = MetaDisposable() private var selectedMessageEffect: AvailableMessageEffects.MessageEffect? - private var standaloneReactionAnimation: LottieMetalAnimatedStickerNode? + private var standaloneReactionAnimation: AnimatedStickerNode? + + private var isLoadingEffectAnimation: Bool = false + private var loadEffectAnimationDisposable: Disposable? private var presentationAnimationState: PresentationAnimationState = .initial private var appliedAnimationState: PresentationAnimationState = .initial @@ -164,6 +176,7 @@ final class ChatSendMessageContextScreenComponent: Component { deinit { self.messageEffectDisposable.dispose() + self.loadEffectAnimationDisposable?.dispose() } @objc private func onBackgroundTap(_ recognizer: UITapGestureRecognizer) { @@ -195,6 +208,20 @@ final class ChatSendMessageContextScreenComponent: Component { self.state?.updated(transition: .spring(duration: 0.4)) } } + + private func requestUpdateOverlayWantsToBeBelowKeyboard(transition: ContainedViewLayoutTransition) { + guard let controller = self.environment?.controller() as? ChatSendMessageContextScreen else { + return + } + controller.overlayWantsToBeBelowKeyboardUpdated(transition: transition) + } + + func wantsToBeBelowKeyboard() -> Bool { + if let reactionContextNode = self.reactionContextNode { + return reactionContextNode.wantsDisplayBelowKeyboard() + } + return false + } func update(component: ChatSendMessageContextScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.isUpdating = true @@ -254,23 +281,32 @@ final class ChatSendMessageContextScreenComponent: Component { ) } - let sendButton: HighlightTrackingButton + let sendButton: SendButton if let current = self.sendButton { sendButton = current } else { - sendButton = HighlightTrackingButton() + sendButton = SendButton() sendButton.accessibilityLabel = environment.strings.MediaPicker_Send sendButton.addTarget(self, action: #selector(self.onSendButtonPressed), for: .touchUpInside) - if let snapshotView = component.sourceSendButton.view.snapshotView(afterScreenUpdates: false) { + /*if let snapshotView = component.sourceSendButton.view.snapshotView(afterScreenUpdates: false) { snapshotView.isUserInteractionEnabled = false sendButton.addSubview(snapshotView) - } + }*/ self.sendButton = sendButton self.addSubview(sendButton) } let sourceSendButtonFrame = convertFrame(component.sourceSendButton.bounds, from: component.sourceSendButton.view, to: self) + sendButton.update( + context: component.context, + presentationData: presentationData, + backgroundNode: component.wallpaperBackgroundNode, + isLoadingEffectAnimation: self.isLoadingEffectAnimation, + size: sourceSendButtonFrame.size, + transition: transition + ) + let sendButtonScale: CGFloat switch self.presentationAnimationState { case .initial: @@ -279,55 +315,6 @@ final class ChatSendMessageContextScreenComponent: Component { sendButtonScale = 1.0 } - let messageItemView: MessageItemView - if let current = self.messageItemView { - messageItemView = current - } else { - messageItemView = MessageItemView(frame: CGRect()) - self.messageItemView = messageItemView - self.addSubview(messageItemView) - } - - let textString: NSAttributedString - if let attributedText = component.textInputView.attributedText { - textString = attributedText - } else { - textString = NSAttributedString(string: " ", font: Font.regular(17.0), textColor: .black) - } - - let localSourceTextInputViewFrame = convertFrame(component.textInputView.bounds, from: component.textInputView, to: self) - - let sourceMessageTextInsets = UIEdgeInsets(top: 7.0, left: 12.0, bottom: 6.0, right: 20.0) - let sourceBackgroundSize = CGSize(width: localSourceTextInputViewFrame.width + 32.0, height: localSourceTextInputViewFrame.height + 4.0) - let explicitMessageBackgroundSize: CGSize? - switch self.presentationAnimationState { - case .initial: - explicitMessageBackgroundSize = sourceBackgroundSize - case .animatedOut: - if self.animateOutToEmpty { - explicitMessageBackgroundSize = nil - } else { - explicitMessageBackgroundSize = sourceBackgroundSize - } - case .animatedIn: - explicitMessageBackgroundSize = nil - } - - let messageTextInsets = sourceMessageTextInsets - - let messageItemSize = messageItemView.update( - context: component.context, - presentationData: presentationData, - backgroundNode: component.wallpaperBackgroundNode, - textString: textString, - textInsets: messageTextInsets, - explicitBackgroundSize: explicitMessageBackgroundSize, - maxTextWidth: localSourceTextInputViewFrame.width - 32.0, - effect: self.presentationAnimationState.key == .animatedIn ? self.selectedMessageEffect : nil, - transition: transition - ) - let sourceMessageItemFrame = CGRect(origin: CGPoint(x: localSourceTextInputViewFrame.minX - sourceMessageTextInsets.left, y: localSourceTextInputViewFrame.minY - 2.0), size: messageItemSize) - let actionsStackNode: ContextControllerActionsStackNode if let current = self.actionsStackNode { actionsStackNode = current @@ -430,7 +417,73 @@ final class ChatSendMessageContextScreenComponent: Component { presentation: .modal, transition: transition.containedViewLayoutTransition ) - let sourceActionsStackFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.minX + 1.0 - actionsStackSize.width, y: sourceMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize) + + let messageItemView: MessageItemView + if let current = self.messageItemView { + messageItemView = current + } else { + messageItemView = MessageItemView(frame: CGRect()) + self.messageItemView = messageItemView + self.addSubview(messageItemView) + } + + let textString: NSAttributedString + if let attributedText = component.textInputView.attributedText { + textString = attributedText + } else { + textString = NSAttributedString(string: " ", font: Font.regular(17.0), textColor: .black) + } + + let localSourceTextInputViewFrame = convertFrame(component.textInputView.bounds, from: component.textInputView, to: self) + + let sourceMessageTextInsets = UIEdgeInsets(top: 7.0, left: 12.0, bottom: 6.0, right: 20.0) + let sourceBackgroundSize = CGSize(width: localSourceTextInputViewFrame.width + 32.0, height: localSourceTextInputViewFrame.height + 4.0) + let explicitMessageBackgroundSize: CGSize? + switch self.presentationAnimationState { + case .initial: + explicitMessageBackgroundSize = sourceBackgroundSize + case .animatedOut: + if self.animateOutToEmpty { + explicitMessageBackgroundSize = nil + } else { + explicitMessageBackgroundSize = sourceBackgroundSize + } + case .animatedIn: + explicitMessageBackgroundSize = nil + } + + let messageTextInsets = sourceMessageTextInsets + + var maxTextHeight: CGFloat = availableSize.height - 8.0 + if let reactionItems = component.reactionItems, !reactionItems.isEmpty { + if let reactionContextNode = self.reactionContextNode, reactionContextNode.isExpanded { + maxTextHeight -= 300.0 + 8.0 + } else { + maxTextHeight -= 60.0 + 14.0 + } + } + maxTextHeight -= environment.statusBarHeight + 14.0 + if environment.inputHeight != 0.0 { + maxTextHeight -= environment.inputHeight + } else { + maxTextHeight -= actionsStackSize.height + maxTextHeight -= environment.safeInsets.bottom + } + + let messageItemSize = messageItemView.update( + context: component.context, + presentationData: presentationData, + backgroundNode: component.wallpaperBackgroundNode, + textString: textString, + sourceTextInputView: component.textInputView as? ChatInputTextView, + textInsets: messageTextInsets, + explicitBackgroundSize: explicitMessageBackgroundSize, + maxTextWidth: localSourceTextInputViewFrame.width, + maxTextHeight: maxTextHeight, + effect: self.presentationAnimationState.key == .animatedIn ? self.selectedMessageEffect : nil, + transition: transition + ) + let sourceMessageItemFrame = CGRect(origin: CGPoint(x: localSourceTextInputViewFrame.minX - sourceMessageTextInsets.left, y: localSourceTextInputViewFrame.minY - 2.0), size: messageItemSize) if let reactionItems = component.reactionItems, !reactionItems.isEmpty { let reactionContextNode: ReactionContextNode @@ -442,7 +495,21 @@ final class ChatSendMessageContextScreenComponent: Component { context: component.context, animationCache: component.context.animationCache, presentationData: presentationData, - items: reactionItems.map(ReactionContextItem.reaction), + items: reactionItems.map { item in + var icon: EmojiPagerContentComponent.Item.Icon = .none + if !component.isPremium, case let .custom(sourceEffectId) = item.reaction.rawValue, let availableMessageEffects = component.availableMessageEffects { + for messageEffect in availableMessageEffects.messageEffects { + if messageEffect.id == sourceEffectId || messageEffect.effectSticker.fileId.id == sourceEffectId { + if messageEffect.isPremium { + icon = .locked + } + break + } + } + } + + return ReactionContextItem.reaction(item: item, icon: icon) + }, selectedItems: Set(), title: "Add an animated effect", reactionsLocked: false, @@ -477,9 +544,7 @@ final class ChatSendMessageContextScreenComponent: Component { guard let self else { return } - if !self.isUpdating { - self.state?.updated(transition: Transition(transition)) - } + self.requestUpdateOverlayWantsToBeBelowKeyboard(transition: transition) } ) reactionContextNode.reactionSelected = { [weak self] updateReaction, _ in @@ -506,11 +571,8 @@ final class ChatSendMessageContextScreenComponent: Component { return nil } - self.messageEffectDisposable.set((combineLatest( - messageEffect, - ReactionContextNode.randomGenericReactionEffect(context: component.context) - ) - |> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect, path in + self.messageEffectDisposable.set((messageEffect + |> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect in guard let self, let component = self.component else { return } @@ -523,6 +585,8 @@ final class ChatSendMessageContextScreenComponent: Component { if selectedMessageEffect.id == effectId { self.selectedMessageEffect = nil reactionContextNode.selectedItems = Set([]) + self.loadEffectAnimationDisposable?.dispose() + self.isLoadingEffectAnimation = false if let standaloneReactionAnimation = self.standaloneReactionAnimation { self.standaloneReactionAnimation = nil @@ -541,6 +605,8 @@ final class ChatSendMessageContextScreenComponent: Component { if !self.isUpdating { self.state?.updated(transition: .easeInOut(duration: 0.2)) } + + HapticFeedback().tap() } } else { self.selectedMessageEffect = messageEffect @@ -548,10 +614,14 @@ final class ChatSendMessageContextScreenComponent: Component { if !self.isUpdating { self.state?.updated(transition: .easeInOut(duration: 0.2)) } + + HapticFeedback().tap() } - guard let targetView = self.messageItemView?.effectIconView else { - return + self.loadEffectAnimationDisposable?.dispose() + self.isLoadingEffectAnimation = true + if !self.isUpdating { + self.state?.updated(transition: .easeInOut(duration: 0.2)) } if let standaloneReactionAnimation = self.standaloneReactionAnimation { @@ -561,56 +631,141 @@ final class ChatSendMessageContextScreenComponent: Component { }) } - let _ = path - - var customEffectResource: MediaResource? + var customEffectResource: (FileMediaReference, MediaResource)? if let effectAnimation = messageEffect.effectAnimation { - customEffectResource = effectAnimation.resource + customEffectResource = (FileMediaReference.standalone(media: effectAnimation), effectAnimation.resource) } else { let effectSticker = messageEffect.effectSticker if let effectFile = effectSticker.videoThumbnails.first { - customEffectResource = effectFile.resource + customEffectResource = (FileMediaReference.standalone(media: effectSticker), effectFile.resource) } } - guard let customEffectResource else { + guard let (customEffectResourceFileReference, customEffectResource) = customEffectResource else { return } - let standaloneReactionAnimation = LottieMetalAnimatedStickerNode() - standaloneReactionAnimation.isUserInteractionEnabled = false - let effectSize = CGSize(width: 380.0, height: 380.0) - var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self)) - effectFrame.origin.x -= effectFrame.width * 0.3 - self.standaloneReactionAnimation = standaloneReactionAnimation - standaloneReactionAnimation.frame = effectFrame - standaloneReactionAnimation.updateLayout(size: effectFrame.size) - self.addSubnode(standaloneReactionAnimation) - - let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(customEffectResource.id) - let source = AnimatedStickerResourceSource(account: component.context.account, resource: customEffectResource, fitzModifier: nil) - standaloneReactionAnimation.setup(source: source, width: Int(effectSize.width), height: Int(effectSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix)) - standaloneReactionAnimation.completed = { [weak self, weak standaloneReactionAnimation] _ in - guard let self else { - return - } - if let standaloneReactionAnimation { - standaloneReactionAnimation.removeFromSupernode() - if self.standaloneReactionAnimation === standaloneReactionAnimation { - self.standaloneReactionAnimation = nil + let context = component.context + var loadEffectAnimationSignal: Signal + loadEffectAnimationSignal = Signal { subscriber in + let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, userLocation: .other, fileReference: customEffectResourceFileReference, resource: customEffectResource).start() + + let dataDisposabke = (context.account.postbox.mediaBox.resourceStatus(customEffectResource) + |> filter { status in + if status == .Local { + return true + } else { + return false } } + |> take(1)).start(next: { _ in + subscriber.putCompletion() + }) + + return ActionDisposable { + fetchDisposable.dispose() + dataDisposabke.dispose() + } } - standaloneReactionAnimation.visibility = true + #if DEBUG + loadEffectAnimationSignal = loadEffectAnimationSignal |> delay(1.0, queue: .mainQueue()) + #endif + + self.loadEffectAnimationDisposable = (loadEffectAnimationSignal + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let self, let component = self.component else { + return + } + + self.isLoadingEffectAnimation = false + + guard let targetView = self.messageItemView?.effectIconView else { + if !self.isUpdating { + self.state?.updated(transition: .easeInOut(duration: 0.2)) + } + return + } + + let standaloneReactionAnimation: AnimatedStickerNode + #if targetEnvironment(simulator) + standaloneReactionAnimation = DirectAnimatedStickerNode() + #else + standaloneReactionAnimation = LottieMetalAnimatedStickerNode() + #endif + + standaloneReactionAnimation.isUserInteractionEnabled = false + let effectSize = CGSize(width: 380.0, height: 380.0) + var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self)) + effectFrame.origin.x -= effectFrame.width * 0.3 + self.standaloneReactionAnimation = standaloneReactionAnimation + standaloneReactionAnimation.frame = effectFrame + standaloneReactionAnimation.updateLayout(size: effectFrame.size) + self.addSubnode(standaloneReactionAnimation) + + let pathPrefix = component.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(customEffectResource.id) + let source = AnimatedStickerResourceSource(account: component.context.account, resource: customEffectResource, fitzModifier: nil) + standaloneReactionAnimation.setup(source: source, width: Int(effectSize.width), height: Int(effectSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix)) + standaloneReactionAnimation.completed = { [weak self, weak standaloneReactionAnimation] _ in + guard let self else { + return + } + if let standaloneReactionAnimation { + standaloneReactionAnimation.removeFromSupernode() + if self.standaloneReactionAnimation === standaloneReactionAnimation { + self.standaloneReactionAnimation = nil + } + } + } + standaloneReactionAnimation.visibility = true + + if !self.isUpdating { + self.state?.updated(transition: .easeInOut(duration: 0.2)) + } + }) })) } + reactionContextNode.premiumReactionsSelected = { [weak self] _ in + guard let self, let component = self.component else { + return + } + //TODO:localize + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + self.environment?.controller()?.present(UndoOverlayController( + presentationData: presentationData, + content: .premiumPaywall( + title: nil, + text: "Subscribe to [TelegramPremium]() to add this animated effect.", + customUndoText: nil, + timeout: nil, + linkAction: nil + ), + elevatedLayout: false, + action: { [weak self] action in + guard let self, let component = self.component else { + return false + } + if case .info = action { + self.window?.endEditing(true) + + //TODO:localize + let premiumController = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .animatedEmoji, forceDark: false, dismissed: nil) + let _ = premiumController + //parentNavigationController.pushViewController(premiumController) + } + return false + } + ), in: .current) + } reactionContextNode.displayTail = true reactionContextNode.forceTailToRight = false reactionContextNode.forceDark = false + reactionContextNode.isMessageEffects = true self.reactionContextNode = reactionContextNode self.addSubview(reactionContextNode.view) } } + let sourceActionsStackFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.minX + 1.0 - actionsStackSize.width, y: sourceMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize) + var readySendButtonFrame = CGRect(origin: CGPoint(x: sourceSendButtonFrame.minX, y: sourceSendButtonFrame.minY), size: sourceSendButtonFrame.size) var readyMessageItemFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 8.0 - messageItemSize.width, y: readySendButtonFrame.maxY - 6.0 - messageItemSize.height), size: messageItemSize) var readyActionsStackFrame = CGRect(origin: CGPoint(x: readySendButtonFrame.minX + 1.0 - actionsStackSize.width, y: readyMessageItemFrame.maxY + messageActionsSpacing), size: actionsStackSize) @@ -622,6 +777,13 @@ final class ChatSendMessageContextScreenComponent: Component { readySendButtonFrame.origin.y -= bottomOverflow } + let inputCoverOverflow = readyMessageItemFrame.maxY + 7.0 - (availableSize.height - environment.inputHeight) + if inputCoverOverflow > 0.0 { + readyMessageItemFrame.origin.y -= inputCoverOverflow + readyActionsStackFrame.origin.y -= inputCoverOverflow + readySendButtonFrame.origin.y -= inputCoverOverflow + } + let messageItemFrame: CGRect let actionsStackFrame: CGRect let sendButtonFrame: CGRect @@ -697,6 +859,7 @@ final class ChatSendMessageContextScreenComponent: Component { transition.setPosition(view: sendButton, position: sendButtonFrame.center) transition.setBounds(view: sendButton, bounds: CGRect(origin: CGPoint(), size: sendButtonFrame.size)) transition.setScale(view: sendButton, scale: sendButtonScale) + sendButton.updateGlobalRect(rect: sendButtonFrame, within: availableSize, transition: transition) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: availableSize)) self.backgroundView.update(size: availableSize, transition: transition.containedViewLayoutTransition) @@ -769,6 +932,14 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha private var processedDidAppear: Bool = false private var processedDidDisappear: Bool = false + override public var overlayWantsToBeBelowKeyboard: Bool { + if let componentView = self.node.hostView.componentView as? ChatSendMessageContextScreenComponent.View { + return componentView.wantsToBeBelowKeyboard() + } else { + return false + } + } + public init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, @@ -786,7 +957,9 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha completion: @escaping () -> Void, sendMessage: @escaping (ChatSendMessageActionSheetController.SendMode, ChatSendMessageActionSheetController.MessageEffect?) -> Void, schedule: @escaping (ChatSendMessageActionSheetController.MessageEffect?) -> Void, - reactionItems: [ReactionItem]? + reactionItems: [ReactionItem]?, + availableMessageEffects: AvailableMessageEffects?, + isPremium: Bool ) { self.context = context @@ -808,7 +981,9 @@ public class ChatSendMessageContextScreen: ViewControllerComponentContainer, Cha completion: completion, sendMessage: sendMessage, schedule: schedule, - reactionItems: reactionItems + reactionItems: reactionItems, + availableMessageEffects: availableMessageEffects, + isPremium: isPremium ), navigationBarAppearance: .none, statusBarStyle: .none, diff --git a/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift b/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift index 58d401ec74..e412c5b76f 100644 --- a/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift +++ b/submodules/ChatSendMessageActionUI/Sources/MessageItemView.swift @@ -17,6 +17,7 @@ import WallpaperBackgroundNode import MultilineTextWithEntitiesComponent import ReactionButtonListComponent import MultilineTextComponent +import ChatInputTextNode private final class EffectIcon: Component { enum Content: Equatable { @@ -135,7 +136,8 @@ final class MessageItemView: UIView { private let backgroundWallpaperNode: ChatMessageBubbleBackdrop private let backgroundNode: ChatMessageBackground - private let text = ComponentView() + private let textClippingContainer: UIView + private var textNode: ChatInputTextNode? private var effectIcon: ComponentView? var effectIconView: UIView? { @@ -150,10 +152,15 @@ final class MessageItemView: UIView { self.backgroundNode = ChatMessageBackground() self.backgroundNode.backdropNode = self.backgroundWallpaperNode + self.textClippingContainer = UIView() + self.textClippingContainer.clipsToBounds = true + super.init(frame: frame) self.addSubview(self.backgroundWallpaperNode.view) self.addSubview(self.backgroundNode.view) + + self.addSubview(self.textClippingContainer) } required init(coder: NSCoder) { @@ -165,9 +172,11 @@ final class MessageItemView: UIView { presentationData: PresentationData, backgroundNode: WallpaperBackgroundNode?, textString: NSAttributedString, + sourceTextInputView: ChatInputTextView?, textInsets: UIEdgeInsets, explicitBackgroundSize: CGSize?, maxTextWidth: CGFloat, + maxTextHeight: CGFloat, effect: AvailableMessageEffects.MessageEffect?, transition: Transition ) -> CGSize { @@ -201,33 +210,84 @@ final class MessageItemView: UIView { if let effectIconSize { textCutout = TextNodeCutout(bottomRight: CGSize(width: effectIconSize.width + 4.0, height: effectIconSize.height)) } + let _ = textCutout - let textSize = self.text.update( - transition: .immediate, - component: AnyComponent(MultilineTextWithEntitiesComponent( - context: context, - animationCache: context.animationCache, - animationRenderer: context.animationRenderer, - placeholderColor: presentationData.theme.chat.message.stickerPlaceholderColor.withWallpaper, - text: .plain(textString), - maximumNumberOfLines: 0, - lineSpacing: 0.0, - cutout: textCutout, - insets: UIEdgeInsets() - )), - environment: {}, - containerSize: CGSize(width: maxTextWidth, height: 20000.0) + let textNode: ChatInputTextNode + if let current = self.textNode { + textNode = current + } else { + textNode = ChatInputTextNode(disableTiling: true) + textNode.textView.isScrollEnabled = false + textNode.isUserInteractionEnabled = false + self.textNode = textNode + self.textClippingContainer.addSubview(textNode.view) + + if let sourceTextInputView { + textNode.textView.defaultTextContainerInset = sourceTextInputView.defaultTextContainerInset + } + + let messageAttributedText = NSMutableAttributedString(attributedString: textString) + messageAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: presentationData.theme.chat.message.outgoing.primaryTextColor, range: NSMakeRange(0, (messageAttributedText.string as NSString).length)) + textNode.attributedText = messageAttributedText + } + + let mainColor = presentationData.theme.chat.message.outgoing.accentControlColor + let mappedLineStyle: ChatInputTextView.Theme.Quote.LineStyle + if let sourceTextInputView, let textTheme = sourceTextInputView.theme { + switch textTheme.quote.lineStyle { + case .solid: + mappedLineStyle = .solid(color: mainColor) + case .doubleDashed: + mappedLineStyle = .doubleDashed(mainColor: mainColor, secondaryColor: .clear) + case .tripleDashed: + mappedLineStyle = .tripleDashed(mainColor: mainColor, secondaryColor: .clear, tertiaryColor: .clear) + } + } else { + mappedLineStyle = .solid(color: mainColor) + } + + textNode.textView.theme = ChatInputTextView.Theme( + quote: ChatInputTextView.Theme.Quote( + background: mainColor.withMultipliedAlpha(0.1), + foreground: mainColor, + lineStyle: mappedLineStyle, + codeBackground: mainColor.withMultipliedAlpha(0.1), + codeForeground: mainColor + ) ) - let size = CGSize(width: textSize.width + textInsets.left + textInsets.right, height: textSize.height + textInsets.top + textInsets.bottom) + let textPositioningInsets = UIEdgeInsets(top: -5.0, left: 0.0, bottom: -4.0, right: -4.0) - let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: textSize) - if let textView = self.text.view { - if textView.superview == nil { - self.addSubview(textView) - } - textView.frame = textFrame + var currentRightInset: CGFloat = 0.0 + if let sourceTextInputView { + currentRightInset = sourceTextInputView.currentRightInset } + let textHeight = textNode.textHeightForWidth(maxTextWidth, rightInset: currentRightInset) + textNode.updateLayout(size: CGSize(width: maxTextWidth, height: textHeight)) + + let textBoundingRect = textNode.textView.currentTextBoundingRect().integral + let lastLineBoundingRect = textNode.textView.lastLineBoundingRect().integral + + let textWidth = textBoundingRect.width + let textSize = CGSize(width: textWidth, height: textHeight) + + var positionedTextSize = CGSize(width: textSize.width + textPositioningInsets.left + textPositioningInsets.right, height: textSize.height + textPositioningInsets.top + textPositioningInsets.bottom) + + let effectInset: CGFloat = 12.0 + if effect != nil, lastLineBoundingRect.width > textSize.width - effectInset { + if lastLineBoundingRect != textBoundingRect { + positionedTextSize.height += 11.0 + } else { + positionedTextSize.width += effectInset + } + } + let unclippedPositionedTextHeight = positionedTextSize.height - (textPositioningInsets.top + textPositioningInsets.bottom) + + positionedTextSize.height = min(positionedTextSize.height, maxTextHeight) + + let size = CGSize(width: positionedTextSize.width + textInsets.left + textInsets.right, height: positionedTextSize.height + textInsets.top + textInsets.bottom) + + let textFrame = CGRect(origin: CGPoint(x: textInsets.left, y: textInsets.top), size: positionedTextSize) let chatTheme: ChatPresentationThemeData if let current = self.chatTheme, current.theme === presentationData.theme { @@ -260,6 +320,21 @@ final class MessageItemView: UIView { let previousSize = self.currentSize self.currentSize = backgroundSize + let textClippingContainerFrame = CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: backgroundSize.width - 1.0 - 7.0, height: backgroundSize.height - 1.0 - 1.0)) + + var textClippingContainerBounds = CGRect(origin: CGPoint(), size: textClippingContainerFrame.size) + if explicitBackgroundSize != nil, let sourceTextInputView { + textClippingContainerBounds.origin.y = sourceTextInputView.contentOffset.y + } else { + textClippingContainerBounds.origin.y = unclippedPositionedTextHeight - backgroundSize.height + 4.0 + textClippingContainerBounds.origin.y = max(0.0, textClippingContainerBounds.origin.y) + } + + transition.setPosition(view: self.textClippingContainer, position: textClippingContainerFrame.center) + transition.setBounds(view: self.textClippingContainer, bounds: textClippingContainerBounds) + + textNode.view.frame = CGRect(origin: CGPoint(x: textFrame.minX + textPositioningInsets.left - textClippingContainerFrame.minX, y: textFrame.minY + textPositioningInsets.top - textClippingContainerFrame.minY), size: CGSize(width: maxTextWidth, height: textHeight)) + if let effectIcon = self.effectIcon, let effectIconSize { if let effectIconView = effectIcon.view { var animateIn = false diff --git a/submodules/ChatSendMessageActionUI/Sources/SendButton.swift b/submodules/ChatSendMessageActionUI/Sources/SendButton.swift new file mode 100644 index 0000000000..0efc276b3b --- /dev/null +++ b/submodules/ChatSendMessageActionUI/Sources/SendButton.swift @@ -0,0 +1,134 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramPresentationData +import AccountContext +import ContextUI +import TelegramCore +import TextFormat +import ReactionSelectionNode +import ViewControllerComponent +import ComponentFlow +import ComponentDisplayAdapters +import ChatMessageBackground +import WallpaperBackgroundNode +import AppBundle +import ActivityIndicator + +final class SendButton: HighlightTrackingButton { + private let containerView: UIView + private var backgroundContent: WallpaperBubbleBackgroundNode? + private let backgroundLayer: SimpleLayer + private let iconView: UIImageView + private var activityIndicator: ActivityIndicator? + + override init(frame: CGRect) { + self.containerView = UIView() + self.containerView.isUserInteractionEnabled = false + + self.backgroundLayer = SimpleLayer() + + self.iconView = UIImageView() + self.iconView.isUserInteractionEnabled = false + + super.init(frame: frame) + + self.containerView.clipsToBounds = true + self.addSubview(self.containerView) + + self.containerView.layer.addSublayer(self.backgroundLayer) + self.containerView.addSubview(self.iconView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update( + context: AccountContext, + presentationData: PresentationData, + backgroundNode: WallpaperBackgroundNode?, + isLoadingEffectAnimation: Bool, + size: CGSize, + transition: Transition + ) { + let innerSize = CGSize(width: 33.0, height: 33.0) + transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: floor((size.width - innerSize.width) * 0.5), y: floor((size.height - innerSize.height) * 0.5)), size: innerSize)) + transition.setCornerRadius(layer: self.containerView.layer, cornerRadius: innerSize.height * 0.5) + + if self.window != nil { + if self.backgroundContent == nil, let backgroundNode = backgroundNode as? WallpaperBackgroundNodeImpl { + if let backgroundContent = backgroundNode.makeLegacyBubbleBackground(for: .outgoing) { + self.backgroundContent = backgroundContent + self.containerView.insertSubview(backgroundContent.view, at: 0) + } + } + } + + if let backgroundContent = self.backgroundContent { + transition.setFrame(view: backgroundContent.view, frame: CGRect(origin: CGPoint(), size: innerSize)) + } + + if [.day, .night].contains(presentationData.theme.referenceTheme.baseTheme) && !presentationData.theme.chat.message.outgoing.bubble.withWallpaper.hasSingleFillColor { + self.backgroundContent?.isHidden = false + self.backgroundLayer.isHidden = true + } else { + self.backgroundContent?.isHidden = true + self.backgroundLayer.isHidden = false + } + + self.backgroundLayer.backgroundColor = presentationData.theme.list.itemAccentColor.cgColor + transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: CGPoint(), size: innerSize)) + + if self.iconView.image == nil { + self.iconView.image = PresentationResourcesChat.chatInputPanelSendIconImage(presentationData.theme) + } + + if let icon = self.iconView.image { + let iconFrame = CGRect(origin: CGPoint(x: floor((innerSize.width - icon.size.width) * 0.5), y: floor((innerSize.height - icon.size.height) * 0.5)), size: icon.size) + transition.setPosition(view: self.iconView, position: iconFrame.center) + transition.setBounds(view: self.iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + transition.setAlpha(view: self.iconView, alpha: isLoadingEffectAnimation ? 0.0 : 1.0) + transition.setScale(view: self.iconView, scale: isLoadingEffectAnimation ? 0.001 : 1.0) + } + + if isLoadingEffectAnimation { + var animateIn = false + let activityIndicator: ActivityIndicator + if let current = self.activityIndicator { + activityIndicator = current + } else { + animateIn = true + activityIndicator = ActivityIndicator(type: .custom(presentationData.theme.list.itemCheckColors.foregroundColor, 18.0, 2.0, true)) + self.activityIndicator = activityIndicator + self.containerView.addSubview(activityIndicator.view) + } + + let activityIndicatorSize = CGSize(width: 18.0, height: 18.0) + let activityIndicatorFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((innerSize.width - activityIndicatorSize.width) * 0.5), y: floor((innerSize.height - activityIndicatorSize.height) * 0.5) + UIScreenPixel), size: activityIndicatorSize) + if animateIn { + activityIndicator.view.frame = activityIndicatorFrame + transition.animateAlpha(view: activityIndicator.view, from: 0.0, to: 1.0) + transition.animateScale(view: activityIndicator.view, from: 0.001, to: 1.0) + } else { + transition.setFrame(view: activityIndicator.view, frame: activityIndicatorFrame) + } + } else { + if let activityIndicator = self.activityIndicator { + self.activityIndicator = nil + transition.setAlpha(view: activityIndicator.view, alpha: 0.0, completion: { [weak activityIndicator] _ in + activityIndicator?.view.removeFromSuperview() + }) + transition.setScale(view: activityIndicator.view, scale: 0.001) + } + } + } + + func updateGlobalRect(rect: CGRect, within containerSize: CGSize, transition: Transition) { + if let backgroundContent = self.backgroundContent { + backgroundContent.update(rect: CGRect(origin: CGPoint(x: rect.minX + self.containerView.frame.minX, y: rect.minY + self.containerView.frame.minY), size: backgroundContent.bounds.size), within: containerSize, transition: transition.containedViewLayoutTransition) + } + } +} diff --git a/submodules/Display/Source/PortalView.swift b/submodules/Display/Source/PortalView.swift index 7762252bce..c26d2632fe 100644 --- a/submodules/Display/Source/PortalView.swift +++ b/submodules/Display/Source/PortalView.swift @@ -22,4 +22,10 @@ public class PortalView { portalSuperlayer.insertSublayer(self.view.layer, at: UInt32(index)) } } + + public func reloadPortal() { + if let sourceView = self.sourceView as? PortalSourceView { + self.reloadPortal(sourceView: sourceView) + } + } } diff --git a/submodules/DrawingUI/Sources/DrawingReactionView.swift b/submodules/DrawingUI/Sources/DrawingReactionView.swift index 0396953a96..959c3525c7 100644 --- a/submodules/DrawingUI/Sources/DrawingReactionView.swift +++ b/submodules/DrawingUI/Sources/DrawingReactionView.swift @@ -133,7 +133,7 @@ public class DrawingReactionEntityView: DrawingStickerEntityView { context: self.context, animationCache: self.context.animationCache, presentationData: self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme), - items: reactionItems.map(ReactionContextItem.reaction), + items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) }, selectedItems: Set(), title: nil, reactionsLocked: false, diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift index 58ce8f1d51..d537ff150f 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextBackgroundNode.swift @@ -41,6 +41,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode { private let backgroundView: BlurredBackgroundView private(set) var vibrancyEffectView: UIVisualEffectView? + let vibrantExpandedContentContainer: UIView private let maskLayer: SimpleLayer private let backgroundClippingLayer: SimpleLayer @@ -84,6 +85,8 @@ final class ReactionContextBackgroundNode: ASDisplayNode { self.smallCircleLayer.cornerCurve = .circular } + self.vibrantExpandedContentContainer = UIView() + super.init() self.layer.addSublayer(self.backgroundShadowLayer) @@ -146,6 +149,7 @@ final class ReactionContextBackgroundNode: ASDisplayNode { let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect) let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect) self.vibrancyEffectView = vibrancyEffectView + vibrancyEffectView.contentView.addSubview(self.vibrantExpandedContentContainer) self.backgroundView.addSubview(vibrancyEffectView) } } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 7a9bec72d8..31b9e01dca 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -82,9 +82,9 @@ public enum ReactionContextItem: Equatable { } else { return false } - case let .reaction(lhsReaction): - if case let .reaction(rhsReaction) = rhs { - return lhsReaction.reaction == rhsReaction.reaction + case let .reaction(lhsReaction, lhsIcon): + if case let .reaction(rhsReaction, rhsIcon) = rhs { + return lhsReaction.reaction == rhsReaction.reaction && lhsIcon == rhsIcon } else { return false } @@ -98,11 +98,11 @@ public enum ReactionContextItem: Equatable { } case staticEmoji(String) - case reaction(ReactionItem) + case reaction(item: ReactionItem, icon: EmojiPagerContentComponent.Item.Icon) case premium public var reaction: ReactionItem.Reaction? { - if case let .reaction(item) = self { + if case let .reaction(item, _) = self { return item.reaction } else { return nil @@ -386,6 +386,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { public var forceDark: Bool = false public var hideBackground: Bool = false + public var isMessageEffects: Bool = false + private var didAnimateIn: Bool = false public private(set) var isAnimatingOut: Bool = false public private(set) var isAnimatingOutToReaction: Bool = false @@ -829,6 +831,8 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { public func wantsDisplayBelowKeyboard() -> Bool { if let emojiView = self.reactionSelectionComponentHost?.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("emoji"))) as? EmojiPagerContentComponent.View { return emojiView.wantsDisplayBelowKeyboard() + } else if let stickersView = self.reactionSelectionComponentHost?.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: AnyHashable("stickers"))) as? EmojiPagerContentComponent.View { + return stickersView.wantsDisplayBelowKeyboard() } else { return false } @@ -1047,8 +1051,16 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { itemTransition = .immediate switch self.items[i] { - case let .reaction(item): - itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: loopIdle, isLocked: self.reactionsLocked) + case let .reaction(item, icon): + var isLocked = self.reactionsLocked + switch icon { + case .locked: + isLocked = true + default: + break + } + + itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: item, icon: icon, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: loopIdle, isLocked: isLocked) maskNode = nil case let .staticEmoji(emoji): itemNode = EmojiItemNode(theme: self.presentationData.theme, emoji: emoji) @@ -1498,6 +1510,9 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { animationOffsetY += 54.0 } else if self.alwaysAllowPremiumReactions { animationOffsetY += 4.0 + } else if self.isMessageEffects { + animationOffsetY += 54.0 + transition.animatePositionAdditive(layer: self.backgroundNode.vibrantExpandedContentContainer.layer, offset: CGPoint(x: 0.0, y: -animationOffsetY + floorToScreenPixels(self.animateFromExtensionDistance / 2.0))) } else { animationOffsetY += 46.0 + 54.0 - 4.0 } @@ -1814,115 +1829,147 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } |> distinctUntilChanged - let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false)) |> then( - context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map { - ($0, true) - } - ) - - let resultSignal = signal - |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in - var allEmoticons: [String: String] = [:] - for keyword in keywords { - for emoticon in keyword.emoticons { - allEmoticons[emoticon] = keyword.keyword - } - } - if isEmojiOnly { - var items: [EmojiPagerContentComponent.Item] = [] - for (_, list) in EmojiPagerContentComponent.staticEmojiMapping { - for emojiString in list { - if allEmoticons[emojiString] != nil { - let item = EmojiPagerContentComponent.Item( - animationData: nil, - content: .staticEmoji(emojiString), - itemFile: nil, - subgroupId: nil, - icon: .none, - tintMode: .none - ) - items.append(item) - } + let resultSignal: Signal<[EmojiPagerContentComponent.ItemGroup], NoError> + if self.isMessageEffects { + resultSignal = signal + |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword } } - var resultGroups: [EmojiPagerContentComponent.ItemGroup] = [] - resultGroups.append(EmojiPagerContentComponent.ItemGroup( - supergroupId: "search", - groupId: "search", - title: nil, - subtitle: nil, - badge: nil, - actionButtonTitle: nil, - isFeatured: false, - isPremiumLocked: false, - isEmbedded: false, - hasClear: false, - hasEdit: false, - collapsedLineCount: nil, - displayPremiumBadges: false, - headerItem: nil, - fillWithLoadingPlaceholders: false, - items: items - )) - return .single(resultGroups) - } else { - return combineLatest( - context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), - context.engine.stickers.availableReactions() |> take(1), - hasPremium |> take(1), - remotePacksSignal - ) - |> map { view, availableReactions, hasPremium, foundPacks -> [EmojiPagerContentComponent.ItemGroup] in - var result: [(String, TelegramMediaFile?, String)] = [] + + return context.availableMessageEffects + |> take(1) + |> mapToSignal { availableMessageEffects -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + guard let availableMessageEffects else { + return .single([]) + } - var allEmoticons: [String: String] = [:] - for keyword in keywords { - for emoticon in keyword.emoticons { - allEmoticons[emoticon] = keyword.keyword + var filteredEffects: [AvailableMessageEffects.MessageEffect] = [] + for messageEffect in availableMessageEffects.messageEffects { + if allEmoticons[messageEffect.emoticon] != nil { + filteredEffects.append(messageEffect) } } - for entry in view.entries { - guard let item = entry.item as? StickerPackItem else { - continue - } - for attribute in item.file.attributes { - switch attribute { - case let .CustomEmoji(_, _, alt, _): - if !item.file.isPremiumEmoji || hasPremium { - if !alt.isEmpty, let keyword = allEmoticons[alt] { - result.append((alt, item.file, keyword)) - } else if alt == query { - result.append((alt, item.file, alt)) - } - } - default: - break - } + var reactionEffects: [AvailableMessageEffects.MessageEffect] = [] + var stickerEffects: [AvailableMessageEffects.MessageEffect] = [] + for messageEffect in filteredEffects { + if messageEffect.effectAnimation != nil { + reactionEffects.append(messageEffect) + } else { + stickerEffects.append(messageEffect) } } - var items: [EmojiPagerContentComponent.Item] = [] + struct ItemGroup { + var supergroupId: AnyHashable + var id: AnyHashable + var title: String? + var subtitle: String? + var actionButtonTitle: String? + var isPremiumLocked: Bool + var isFeatured: Bool + var displayPremiumBadges: Bool + var hasEdit: Bool + var headerItem: EntityKeyboardAnimationData? + var items: [EmojiPagerContentComponent.Item] + } - var existingIds = Set() - for item in result { - if let itemFile = item.1 { - if existingIds.contains(itemFile.fileId) { - continue + var resultGroups: [ItemGroup] = [] + var resultGroupIndexById: [AnyHashable: Int] = [:] + + for i in 0 ..< 2 { + let groupId = i == 0 ? "reactions" : "stickers" + for item in i == 0 ? reactionEffects : stickerEffects { + let itemFile: TelegramMediaFile = item.effectSticker + + var tintMode: EmojiPagerContentComponent.Item.TintMode = .none + if itemFile.isCustomTemplateEmoji { + tintMode = .primary } - existingIds.insert(itemFile.fileId) - let animationData = EntityKeyboardAnimationData(file: itemFile) - let item = EmojiPagerContentComponent.Item( + + let animationData = EntityKeyboardAnimationData(file: itemFile, partialReference: .none) + let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), - itemFile: itemFile, subgroupId: nil, + itemFile: itemFile, + subgroupId: nil, icon: .none, - tintMode: animationData.isTemplate ? .primary : .none + tintMode: tintMode ) - items.append(item) + + if let groupIndex = resultGroupIndexById[groupId] { + resultGroups[groupIndex].items.append(resultItem) + } else { + resultGroupIndexById[groupId] = resultGroups.count + //TODO:localize + resultGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : "Message Effects", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) + } } } + let allItemGroups = resultGroups.map { group -> EmojiPagerContentComponent.ItemGroup in + let hasClear = false + let isEmbedded = false + + return EmojiPagerContentComponent.ItemGroup( + supergroupId: group.supergroupId, + groupId: group.id, + title: group.title, + subtitle: group.subtitle, + badge: nil, + actionButtonTitle: group.actionButtonTitle, + isFeatured: group.isFeatured, + isPremiumLocked: group.isPremiumLocked, + isEmbedded: isEmbedded, + hasClear: hasClear, + hasEdit: group.hasEdit, + collapsedLineCount: nil, + displayPremiumBadges: group.displayPremiumBadges, + headerItem: group.headerItem, + fillWithLoadingPlaceholders: false, + items: group.items + ) + } + + return .single(allItemGroups) + } + } + } else { + let remotePacksSignal: Signal<(sets: FoundStickerSets, isFinalResult: Bool), NoError> = .single((FoundStickerSets(), false)) |> then( + context.engine.stickers.searchEmojiSetsRemotely(query: query) |> map { + ($0, true) + } + ) + + resultSignal = signal + |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + } + } + if isEmojiOnly { + var items: [EmojiPagerContentComponent.Item] = [] + for (_, list) in EmojiPagerContentComponent.staticEmojiMapping { + for emojiString in list { + if allEmoticons[emojiString] != nil { + let item = EmojiPagerContentComponent.Item( + animationData: nil, + content: .staticEmoji(emojiString), + itemFile: nil, + subgroupId: nil, + icon: .none, + tintMode: .none + ) + items.append(item) + } + } + } var resultGroups: [EmojiPagerContentComponent.ItemGroup] = [] resultGroups.append(EmojiPagerContentComponent.ItemGroup( supergroupId: "search", @@ -1942,59 +1989,138 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { fillWithLoadingPlaceholders: false, items: items )) - - for (collectionId, info, _, _) in foundPacks.sets.infos { - if let info = info as? StickerPackCollectionInfo { - var topItems: [StickerPackItem] = [] - for e in foundPacks.sets.entries { - if let item = e.item as? StickerPackItem { - if e.index.collectionId == collectionId { - topItems.append(item) + return .single(resultGroups) + } else { + return combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000) |> take(1), + context.engine.stickers.availableReactions() |> take(1), + hasPremium |> take(1), + remotePacksSignal + ) + |> map { view, availableReactions, hasPremium, foundPacks -> [EmojiPagerContentComponent.ItemGroup] in + var result: [(String, TelegramMediaFile?, String)] = [] + + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + } + } + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + for attribute in item.file.attributes { + switch attribute { + case let .CustomEmoji(_, _, alt, _): + if !item.file.isPremiumEmoji || hasPremium { + if !alt.isEmpty, let keyword = allEmoticons[alt] { + result.append((alt, item.file, keyword)) + } else if alt == query { + result.append((alt, item.file, alt)) + } } + default: + break } } - - var groupItems: [EmojiPagerContentComponent.Item] = [] - for item in topItems { - var tintMode: EmojiPagerContentComponent.Item.TintMode = .none - if item.file.isCustomTemplateEmoji { - tintMode = .primary + } + + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for item in result { + if let itemFile = item.1 { + if existingIds.contains(itemFile.fileId) { + continue } - - let animationData = EntityKeyboardAnimationData(file: item.file) - let resultItem = EmojiPagerContentComponent.Item( + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), - itemFile: item.file, - subgroupId: nil, + itemFile: itemFile, subgroupId: nil, icon: .none, - tintMode: tintMode + tintMode: animationData.isTemplate ? .primary : .none ) - - groupItems.append(resultItem) + items.append(item) } - - resultGroups.append(EmojiPagerContentComponent.ItemGroup( - supergroupId: AnyHashable(info.id), - groupId: AnyHashable(info.id), - title: info.title, - subtitle: nil, - badge: nil, - actionButtonTitle: nil, - isFeatured: false, - isPremiumLocked: false, - isEmbedded: false, - hasClear: false, - hasEdit: false, - collapsedLineCount: 3, - displayPremiumBadges: false, - headerItem: nil, - fillWithLoadingPlaceholders: false, - items: groupItems - )) } + + var resultGroups: [EmojiPagerContentComponent.ItemGroup] = [] + resultGroups.append(EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: items + )) + + for (collectionId, info, _, _) in foundPacks.sets.infos { + if let info = info as? StickerPackCollectionInfo { + var topItems: [StickerPackItem] = [] + for e in foundPacks.sets.entries { + if let item = e.item as? StickerPackItem { + if e.index.collectionId == collectionId { + topItems.append(item) + } + } + } + + var groupItems: [EmojiPagerContentComponent.Item] = [] + for item in topItems { + var tintMode: EmojiPagerContentComponent.Item.TintMode = .none + if item.file.isCustomTemplateEmoji { + tintMode = .primary + } + + let animationData = EntityKeyboardAnimationData(file: item.file) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: item.file, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + groupItems.append(resultItem) + } + + resultGroups.append(EmojiPagerContentComponent.ItemGroup( + supergroupId: AnyHashable(info.id), + groupId: AnyHashable(info.id), + title: info.title, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: 3, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: groupItems + )) + } + } + return resultGroups } - return resultGroups } } } @@ -2013,45 +2139,156 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { })) } case let .category(value): - let resultSignal = self.context.engine.stickers.searchEmoji(category: value) - |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in - var items: [EmojiPagerContentComponent.Item] = [] - - var existingIds = Set() - for itemFile in files { - if existingIds.contains(itemFile.fileId) { - continue + let context = self.context + let resultSignal: Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> + if self.isMessageEffects { + let keywords: Signal<[String], NoError> = .single(value.identifiers) + resultSignal = keywords + |> mapToSignal { keywords -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in + var allEmoticons: [String: String] = [:] + for keyword in keywords { + allEmoticons[keyword] = keyword + } + + return context.availableMessageEffects + |> take(1) + |> mapToSignal { availableMessageEffects -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in + guard let availableMessageEffects else { + return .single(([], true)) + } + + var filteredEffects: [AvailableMessageEffects.MessageEffect] = [] + for messageEffect in availableMessageEffects.messageEffects { + if allEmoticons[messageEffect.emoticon] != nil { + filteredEffects.append(messageEffect) + } + } + + var reactionEffects: [AvailableMessageEffects.MessageEffect] = [] + var stickerEffects: [AvailableMessageEffects.MessageEffect] = [] + for messageEffect in filteredEffects { + if messageEffect.effectAnimation != nil { + reactionEffects.append(messageEffect) + } else { + stickerEffects.append(messageEffect) + } + } + + struct ItemGroup { + var supergroupId: AnyHashable + var id: AnyHashable + var title: String? + var subtitle: String? + var actionButtonTitle: String? + var isPremiumLocked: Bool + var isFeatured: Bool + var displayPremiumBadges: Bool + var hasEdit: Bool + var headerItem: EntityKeyboardAnimationData? + var items: [EmojiPagerContentComponent.Item] + } + + var resultGroups: [ItemGroup] = [] + var resultGroupIndexById: [AnyHashable: Int] = [:] + + for i in 0 ..< 2 { + let groupId = i == 0 ? "reactions" : "stickers" + for item in i == 0 ? reactionEffects : stickerEffects { + let itemFile: TelegramMediaFile = item.effectSticker + + var tintMode: EmojiPagerContentComponent.Item.TintMode = .none + if itemFile.isCustomTemplateEmoji { + tintMode = .primary + } + + let animationData = EntityKeyboardAnimationData(file: itemFile, partialReference: .none) + let resultItem = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, + subgroupId: nil, + icon: .none, + tintMode: tintMode + ) + + if let groupIndex = resultGroupIndexById[groupId] { + resultGroups[groupIndex].items.append(resultItem) + } else { + resultGroupIndexById[groupId] = resultGroups.count + //TODO:localize + resultGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : "Message Effects", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) + } + } + } + + let allItemGroups = resultGroups.map { group -> EmojiPagerContentComponent.ItemGroup in + let hasClear = false + let isEmbedded = false + + return EmojiPagerContentComponent.ItemGroup( + supergroupId: group.supergroupId, + groupId: group.id, + title: group.title, + subtitle: group.subtitle, + badge: nil, + actionButtonTitle: group.actionButtonTitle, + isFeatured: group.isFeatured, + isPremiumLocked: group.isPremiumLocked, + isEmbedded: isEmbedded, + hasClear: hasClear, + hasEdit: group.hasEdit, + collapsedLineCount: nil, + displayPremiumBadges: group.displayPremiumBadges, + headerItem: group.headerItem, + fillWithLoadingPlaceholders: false, + items: group.items + ) + } + + return .single((allItemGroups, true)) } - existingIds.insert(itemFile.fileId) - let animationData = EntityKeyboardAnimationData(file: itemFile) - let item = EmojiPagerContentComponent.Item( - animationData: animationData, - content: .animation(animationData), - itemFile: itemFile, subgroupId: nil, - icon: .none, - tintMode: animationData.isTemplate ? .primary : .none - ) - items.append(item) } - - return .single(([EmojiPagerContentComponent.ItemGroup( - supergroupId: "search", - groupId: "search", - title: nil, - subtitle: nil, - badge: nil, - actionButtonTitle: nil, - isFeatured: false, - isPremiumLocked: false, - isEmbedded: false, - hasClear: false, - hasEdit: false, - collapsedLineCount: nil, - displayPremiumBadges: false, - headerItem: nil, - fillWithLoadingPlaceholders: false, - items: items - )], isFinalResult)) + } else { + resultSignal = self.context.engine.stickers.searchEmoji(category: value) + |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for itemFile in files { + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + items.append(item) + } + + return .single(([EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + badge: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + hasEdit: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: items + )], isFinalResult)) + } } var version = 0 @@ -2101,7 +2338,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { peekBehavior: nil, customLayout: emojiContentLayout, externalBackground: self.backgroundNode.vibrancyEffectView == nil ? nil : EmojiPagerContentComponent.ExternalBackground( - effectContainerView: self.backgroundNode.vibrancyEffectView?.contentView + effectContainerView: self.backgroundNode.vibrantExpandedContentContainer ), externalExpansionView: self.view, customContentView: nil, @@ -2310,7 +2547,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { } if let customReactionSource = self.customReactionSource { - let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, isLocked: false, useDirectRendering: false) + let itemNode = ReactionNode(context: self.context, theme: self.presentationData.theme, item: customReactionSource.item, icon: .none, animationCache: self.animationCache, animationRenderer: self.animationRenderer, loopIdle: false, isLocked: false, useDirectRendering: false) if let contents = customReactionSource.layer.contents { itemNode.setCustomContents(contents: contents) } @@ -2737,11 +2974,13 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { self.isExpandedUpdated(.animated(duration: 0.4, curve: .spring)) } else if let reaction = self.reaction(at: point) { switch reaction { - case let .reaction(reactionItem): + case let .reaction(reactionItem, icon): if case .custom = reactionItem.updateMessageReaction, let hasPremium = self.hasPremium, !hasPremium, !self.allPresetReactionsAreAvailable { self.premiumReactionsSelected?(reactionItem.stillAnimation) } else if self.reactionsLocked { self.premiumReactionsSelected?(reactionItem.stillAnimation) + } else if case .locked = icon { + self.premiumReactionsSelected?(reactionItem.stillAnimation) } else { self.reactionSelected?(reactionItem.updateMessageReaction, false) } @@ -2915,7 +3154,7 @@ public final class ReactionContextNode: ASDisplayNode, ASScrollViewDelegate { if !itemNode.isAnimationLoaded { return nil } - return .reaction(itemNode.item) + return .reaction(item: itemNode.item, icon: itemNode.icon) } else if let itemNode = itemNode as? EmojiItemNode { return .staticEmoji(itemNode.emoji) } else if let _ = itemNode as? PremiumReactionsNode { @@ -2999,7 +3238,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { itemNode = currentItemNode } else { let animationRenderer = MultiAnimationRendererImpl() - itemNode = ReactionNode(context: context, theme: theme, item: reaction, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false) + itemNode = ReactionNode(context: context, theme: theme, item: reaction, icon: .none, animationCache: animationCache, animationRenderer: animationRenderer, loopIdle: false, isLocked: false) } self.itemNode = itemNode } else { diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 43cef28234..9140f9745a 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -14,6 +14,7 @@ import AnimationCache import MultiAnimationRenderer import ShimmerEffect import GenerateStickerPlaceholderImage +import EntityKeyboard private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in @@ -52,13 +53,14 @@ protocol ReactionItemNode: ASDisplayNode { func updateLayout(size: CGSize, isExpanded: Bool, largeExpanded: Bool, isPreviewing: Bool, transition: ContainedViewLayoutTransition) } -private let lockedBackgroundImage: UIImage = generateFilledCircleImage(diameter: 12.0, color: .white)!.withRenderingMode(.alwaysTemplate) +private let lockedBackgroundImage: UIImage = generateFilledCircleImage(diameter: 16.0, color: .white)!.withRenderingMode(.alwaysTemplate) private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white) public final class ReactionNode: ASDisplayNode, ReactionItemNode { let context: AccountContext let theme: PresentationTheme let item: ReactionItem + let icon: EmojiPagerContentComponent.Item.Icon private let loopIdle: Bool private let isLocked: Bool private let hasAppearAnimation: Bool @@ -102,10 +104,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { return self.staticAnimationNode.currentFrameImage != nil } - public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, loopIdle: Bool, isLocked: Bool, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) { + public init(context: AccountContext, theme: PresentationTheme, item: ReactionItem, icon: EmojiPagerContentComponent.Item.Icon, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, loopIdle: Bool, isLocked: Bool, hasAppearAnimation: Bool = true, useDirectRendering: Bool = false) { self.context = context self.theme = theme self.item = item + self.icon = icon self.loopIdle = loopIdle self.isLocked = isLocked self.hasAppearAnimation = hasAppearAnimation @@ -475,11 +478,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode { } if let lockBackgroundView = self.lockBackgroundView, let lockIconView = self.lockIconView, let iconImage = lockIconView.image { - let lockSize: CGFloat = 12.0 + let lockSize: CGFloat = 16.0 let iconBackgroundFrame = CGRect(origin: CGPoint(x: animationFrame.maxX - lockSize, y: animationFrame.maxY - lockSize), size: CGSize(width: lockSize, height: lockSize)) transition.updateFrame(view: lockBackgroundView, frame: iconBackgroundFrame) - let iconFactor: CGFloat = 0.7 + let iconFactor: CGFloat = 1.0 let iconImageSize = CGSize(width: floor(iconImage.size.width * iconFactor), height: floor(iconImage.size.height * iconFactor)) transition.updateFrame(view: lockIconView, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floorToScreenPixels((iconBackgroundFrame.width - iconImageSize.width) * 0.5), y: iconBackgroundFrame.minY + floorToScreenPixels((iconBackgroundFrame.height - iconImageSize.height) * 0.5)), size: iconImageSize)) diff --git a/submodules/TelegramCore/Sources/State/AvailableMessageEffects.swift b/submodules/TelegramCore/Sources/State/AvailableMessageEffects.swift index df2f76f248..3d114bbf8b 100644 --- a/submodules/TelegramCore/Sources/State/AvailableMessageEffects.swift +++ b/submodules/TelegramCore/Sources/State/AvailableMessageEffects.swift @@ -156,7 +156,7 @@ private extension AvailableMessageEffects.MessageEffect { return nil } - let isPremium = (flags & (1 << 3)) != 0 + let isPremium = (flags & (1 << 2)) != 0 self.init( id: id, isPremium: isPremium, @@ -239,7 +239,7 @@ func managedSynchronizeAvailableMessageEffects(postbox: Postbox, network: Networ break } - var signals: [Signal] = [] + /*var signals: [Signal] = [] if let availableMessageEffects = _internal_cachedAvailableMessageEffects(transaction: transaction) { var resources: [MediaResource] = [] @@ -271,7 +271,9 @@ func managedSynchronizeAvailableMessageEffects(postbox: Postbox, network: Networ } return combineLatest(signals) - |> ignoreValues + |> ignoreValues*/ + + return .complete() } |> switchToLatest }) diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift index 2c3823e1f3..ca6cad9775 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift @@ -312,6 +312,10 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate, } } + public var currentRightInset: CGFloat { + return self.customTextContainer.rightInset + } + private var didInitializePrimaryInputLanguage: Bool = false public var initialPrimaryLanguage: String? @@ -656,6 +660,45 @@ public final class ChatInputTextView: ChatInputTextViewImpl, UITextViewDelegate, self.isUpdatingLayout = false } + public func currentTextBoundingRect() -> CGRect { + let glyphRange = self.customLayoutManager.glyphRange(forCharacterRange: NSRange(location: 0, length: self.textStorage.length), actualCharacterRange: nil) + + var boundingRect = CGRect() + var startIndex = glyphRange.lowerBound + while startIndex < glyphRange.upperBound { + var effectiveRange = NSRange(location: NSNotFound, length: 0) + let rect = self.customLayoutManager.lineFragmentUsedRect(forGlyphAt: startIndex, effectiveRange: &effectiveRange) + if boundingRect.isEmpty { + boundingRect = rect + } else { + boundingRect = boundingRect.union(rect) + } + if effectiveRange.location != NSNotFound { + startIndex = max(startIndex + 1, effectiveRange.upperBound) + } else { + break + } + } + return boundingRect + } + + public func lastLineBoundingRect() -> CGRect { + let glyphRange = self.customLayoutManager.glyphRange(forCharacterRange: NSRange(location: 0, length: self.textStorage.length), actualCharacterRange: nil) + var boundingRect = CGRect() + var startIndex = glyphRange.lowerBound + while startIndex < glyphRange.upperBound { + var effectiveRange = NSRange(location: NSNotFound, length: 0) + let rect = self.customLayoutManager.lineFragmentUsedRect(forGlyphAt: startIndex, effectiveRange: &effectiveRange) + boundingRect = rect + if effectiveRange.location != NSNotFound { + startIndex = max(startIndex + 1, effectiveRange.upperBound) + } else { + break + } + } + return boundingRect + } + public func updateTextElements() { var blockQuoteIndex = 0 var validBlockQuotes: [Int] = [] diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 3996a553ef..44a2e04d39 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -633,6 +633,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI } self.visibilityStatus = self.visibility != .none + + self.updateVisibility() } } } @@ -648,8 +650,6 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI containerSize: credibilityIconView.bounds.size ) } - - self.updateVisibility() } } } @@ -5840,7 +5840,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI do { let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id) - let additionalAnimationNode = LottieMetalAnimatedStickerNode() + let additionalAnimationNode: AnimatedStickerNode + #if targetEnvironment(simulator) + additionalAnimationNode = DirectAnimatedStickerNode() + #else + additionalAnimationNode = LottieMetalAnimatedStickerNode() + #endif additionalAnimationNode.updateLayout(size: animationSize) additionalAnimationNode.setup(source: source, width: Int(animationSize.width), height: Int(animationSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix)) var animationFrame: CGRect @@ -5925,32 +5930,46 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI return } - let isPlaying = self.visibilityStatus == true && !self.forceStopAnimations + var isPlaying = true + if case let .visible(_, subRect) = self.visibility { + if subRect.minY > 32.0 { + isPlaying = false + } + } else { + isPlaying = false + } + + if self.forceStopAnimations { + isPlaying = false + } + if !isPlaying { self.removeAdditionalAnimations() } - var alreadySeen = true - if item.message.flags.contains(.Incoming) { - if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] { - if unreadRange.contains(item.message.id.id) { + if isPlaying { + var alreadySeen = true + if item.message.flags.contains(.Incoming) { + if let unreadRange = item.controllerInteraction.unreadMessageRange[UnreadMessageRangeKey(peerId: item.message.id.peerId, namespace: item.message.id.namespace)] { + if unreadRange.contains(item.message.id.id) { + if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) { + alreadySeen = false + } + } + } + } else { + if self.didChangeFromPendingToSent { if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) { alreadySeen = false } } } - } else { - if self.didChangeFromPendingToSent { - if !item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) { - alreadySeen = false - } - } - } - - if !alreadySeen { - item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id) - self.playMessageEffect(force: false) + if !alreadySeen { + item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id) + + self.playMessageEffect(force: false) + } } } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift index dac4218fd7..d1a1ac3563 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageSelectionInputPanelNode/Sources/ChatMessageSelectionInputPanelNode.swift @@ -257,7 +257,7 @@ public final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { context: context, animationCache: context.animationCache, presentationData: presentationData, - items: reactionItems.map(ReactionContextItem.reaction), + items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) }, selectedItems: actions.editTags, title: actions.editTags.isEmpty ? presentationData.strings.Chat_ReactionSelectionTitleAddTag : presentationData.strings.Chat_ReactionSelectionTitleEditTag, reactionsLocked: false, diff --git a/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/Sources/ChatShareMessageTagView.swift b/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/Sources/ChatShareMessageTagView.swift index 958a9dad1d..f2e78e7f46 100644 --- a/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/Sources/ChatShareMessageTagView.swift +++ b/submodules/TelegramUI/Components/Chat/ChatShareMessageTagView/Sources/ChatShareMessageTagView.swift @@ -31,7 +31,7 @@ public final class ChatShareMessageTagView: UIView, UndoOverlayControllerAdditio context: context, animationCache: context.animationCache, presentationData: presentationData, - items: reactionItems.map(ReactionContextItem.reaction), + items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) }, selectedItems: Set(), title: isSingleMessage ? presentationData.strings.Chat_ForwardToSavedMessageTagSelectionTitle : presentationData.strings.Chat_ForwardToSavedMessagesTagSelectionTitle, reactionsLocked: false, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index a08c0ef49f..3e7e6d2904 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -47,6 +47,7 @@ swift_library( "//submodules/rlottie:RLottieBinding", "//submodules/lottie-ios:Lottie", "//submodules/TelegramUI/Components/Utils/GenerateStickerPlaceholderImage", + "//submodules/TelegramUIPreferences", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 30d4ec87a1..da9153aea9 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -602,14 +602,23 @@ private class PassthroughShapeLayer: CAShapeLayer { } } +private let itemBadgeTextFont: UIFont = { + return Font.regular(10.0) +}() + private final class PremiumBadgeView: UIView { + private let context: AccountContext + private var badge: EmojiPagerContentComponent.View.ItemLayer.Badge? let contentLayer: SimpleLayer private let overlayColorLayer: SimpleLayer private let iconLayer: SimpleLayer + private var customFileLayer: InlineFileIconLayer? - init() { + init(context: AccountContext) { + self.context = context + self.contentLayer = SimpleLayer() self.contentLayer.contentsGravity = .resize self.contentLayer.masksToBounds = true @@ -641,6 +650,47 @@ private final class PremiumBadgeView: UIView { self.iconLayer.contents = featuredBadgeIcon?.cgImage case .locked: self.iconLayer.contents = lockedBadgeIcon?.cgImage + case let .text(text): + let string = NSAttributedString(string: text, font: itemBadgeTextFont) + let size = CGSize(width: 12.0, height: 12.0) + let stringBounds = string.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + let image = generateImage(size, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + string.draw(at: CGPoint(x: floor((size.width - stringBounds.width) * 0.5), y: floor((size.height - stringBounds.height) * 0.5))) + UIGraphicsPopContext() + }) + self.iconLayer.contents = image?.cgImage + case .customFile: + self.iconLayer.contents = nil + } + + if case let .customFile(customFile) = badge { + let customFileLayer: InlineFileIconLayer + if let current = self.customFileLayer { + customFileLayer = current + } else { + customFileLayer = InlineFileIconLayer( + context: self.context, + userLocation: .other, + attemptSynchronousLoad: false, + file: customFile, + cache: self.context.animationCache, + renderer: self.context.animationRenderer, + unique: false, + placeholderColor: .clear, + pointSize: CGSize(width: 18.0, height: 18.0), + dynamicColor: nil + ) + self.customFileLayer = customFileLayer + self.layer.addSublayer(customFileLayer) + } + let _ = customFileLayer + } else { + if let customFileLayer = self.customFileLayer { + self.customFileLayer = nil + customFileLayer.removeFromSuperlayer() + } } } @@ -652,6 +702,17 @@ private final class PremiumBadgeView: UIView { iconInset = 0.0 case .locked: iconInset = 0.0 + case .text, .customFile: + iconInset = 0.0 + } + + switch badge { + case .text, .customFile: + self.contentLayer.isHidden = true + self.overlayColorLayer.isHidden = true + default: + self.contentLayer.isHidden = false + self.overlayColorLayer.isHidden = false } self.overlayColorLayer.backgroundColor = backgroundColor.cgColor @@ -663,6 +724,11 @@ private final class PremiumBadgeView: UIView { transition.setCornerRadius(layer: self.overlayColorLayer, cornerRadius: min(size.width / 2.0, size.height / 2.0)) transition.setFrame(layer: self.iconLayer, frame: CGRect(origin: CGPoint(), size: size).insetBy(dx: iconInset, dy: iconInset)) + + if let customFileLayer = self.customFileLayer { + let iconSize = CGSize(width: 18.0, height: 18.0) + transition.setFrame(layer: customFileLayer, frame: CGRect(origin: CGPoint(), size: iconSize)) + } } } @@ -2608,6 +2674,8 @@ public final class EmojiPagerContentComponent: Component { case none case locked case premium + case text(String) + case customFile(TelegramMediaFile) } public enum TintMode: Equatable { @@ -3448,13 +3516,16 @@ public final class EmojiPagerContentComponent: Component { } } - enum Badge { + enum Badge: Equatable { case premium case locked case featured + case text(String) + case customFile(TelegramMediaFile) } public let item: Item + private let context: AccountContext private var content: ItemContent private var theme: PresentationTheme? @@ -3566,6 +3637,7 @@ public final class EmojiPagerContentComponent: Component { onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void ) { self.item = item + self.context = context self.content = content self.placeholderColor = placeholderColor self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder @@ -3717,6 +3789,7 @@ public final class EmojiPagerContentComponent: Component { preconditionFailure() } + self.context = layer.context self.item = layer.item self.content = layer.content @@ -3837,7 +3910,7 @@ public final class EmojiPagerContentComponent: Component { premiumBadgeView = current } else { badgeTransition = .immediate - premiumBadgeView = PremiumBadgeView() + premiumBadgeView = PremiumBadgeView(context: self.context) self.premiumBadgeView = premiumBadgeView self.addSublayer(premiumBadgeView.layer) } @@ -6202,6 +6275,10 @@ public final class EmojiPagerContentComponent: Component { badge = .locked case .premium: badge = .premium + case let .text(value): + badge = .text(value) + case let .customFile(customFile): + badge = .customFile(customFile) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift index 329943a079..32c06a95bf 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentSignals.swift @@ -2176,7 +2176,7 @@ public extension EmojiPagerContentComponent { let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings - let searchCategories: Signal = .single(nil) + let searchCategories: Signal = context.engine.stickers.emojiSearchCategories(kind: .emoji) return combineLatest( hasPremium(context: context, chatPeerId: nil, premiumIfSavedMessages: false), @@ -2225,13 +2225,30 @@ public extension EmojiPagerContentComponent { tintMode = .primary } + let icon: EmojiPagerContentComponent.Item.Icon + if i == 0 { + if !hasPremium && item.isPremium { + icon = .locked + } else { + icon = .none + } + } else { + if !hasPremium && item.isPremium { + icon = .locked + } else if let staticIcon = item.staticIcon { + icon = .customFile(staticIcon) + } else { + icon = .text(item.emoticon) + } + } + let animationData = EntityKeyboardAnimationData(file: itemFile, partialReference: .none) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: itemFile, subgroupId: nil, - icon: .none, + icon: icon, tintMode: tintMode ) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift new file mode 100644 index 0000000000..7de681f13f --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/InlineFileIconLayer.swift @@ -0,0 +1,375 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import TelegramPresentationData +import TelegramCore +import Postbox +import SwiftSignalKit +import MultiAnimationRenderer +import AnimationCache +import AccountContext +import TelegramUIPreferences +import GenerateStickerPlaceholderImage +import EmojiTextAttachmentView +import LottieAnimationCache + +public final class InlineFileIconLayer: MultiAnimationRenderTarget { + private final class Arguments { + let context: InlineFileIconLayer.Context + let userLocation: MediaResourceUserLocation + let file: TelegramMediaFile + let cache: AnimationCache + let renderer: MultiAnimationRenderer + let unique: Bool + let placeholderColor: UIColor + + let pointSize: CGSize + let pixelSize: CGSize + + init(context: InlineFileIconLayer.Context, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, cache: AnimationCache, renderer: MultiAnimationRenderer, unique: Bool, placeholderColor: UIColor, pointSize: CGSize, pixelSize: CGSize) { + self.context = context + self.userLocation = userLocation + self.file = file + self.cache = cache + self.renderer = renderer + self.unique = unique + self.placeholderColor = placeholderColor + self.pointSize = pointSize + self.pixelSize = pixelSize + } + } + + public enum Context: Equatable { + public final class Custom: Equatable { + public let postbox: Postbox + public let energyUsageSettings: () -> EnergyUsageSettings + public let resolveInlineStickers: ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> + + public init(postbox: Postbox, energyUsageSettings: @escaping () -> EnergyUsageSettings, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>) { + self.postbox = postbox + self.energyUsageSettings = energyUsageSettings + self.resolveInlineStickers = resolveInlineStickers + } + + public static func ==(lhs: Custom, rhs: Custom) -> Bool { + if lhs.postbox !== rhs.postbox { + return false + } + return true + } + } + + case account(AccountContext) + case custom(Custom) + + var postbox: Postbox { + switch self { + case let .account(account): + return account.account.postbox + case let .custom(custom): + return custom.postbox + } + } + + var energyUsageSettings: EnergyUsageSettings { + switch self { + case let .account(account): + return account.sharedContext.energyUsageSettings + case let .custom(custom): + return custom.energyUsageSettings() + } + } + + func resolveInlineStickers(fileIds: [Int64]) -> Signal<[Int64: TelegramMediaFile], NoError> { + switch self { + case let .account(account): + return account.engine.stickers.resolveInlineStickers(fileIds: fileIds) + case let .custom(custom): + return custom.resolveInlineStickers(fileIds) + } + } + + public static func ==(lhs: Context, rhs: Context) -> Bool { + switch lhs { + case let .account(lhsContext): + if case let .account(rhsContext) = rhs, lhsContext === rhsContext { + return true + } else { + return false + } + case let .custom(custom): + if case .custom(custom) = rhs { + return true + } else { + return false + } + } + } + } + + public static let queue = Queue() + + public struct Key: Hashable { + public var id: Int64 + public var index: Int + + public init(id: Int64, index: Int) { + self.id = id + self.index = index + } + } + + private let arguments: Arguments? + + private var isDisplayingPlaceholder: Bool = false + private var didProcessTintColor: Bool = false + + public private(set) var file: TelegramMediaFile? + private var infoDisposable: Disposable? + private var disposable: Disposable? + private var fetchDisposable: Disposable? + private var loadDisposable: Disposable? + + private var _contentTintColor: UIColor? + public var contentTintColor: UIColor? { + get { + return self._contentTintColor + } + set(value) { + if self._contentTintColor != value { + self._contentTintColor = value + } + } + } + + private var _dynamicColor: UIColor? + public var dynamicColor: UIColor? { + get { + return self._dynamicColor + } + set(value) { + if self._dynamicColor != value { + self._dynamicColor = value + } + } + } + + private var currentLoopCount: Int = 0 + + private var isInHierarchyValue: Bool = false + + public convenience init( + context: AccountContext, + userLocation: MediaResourceUserLocation, + attemptSynchronousLoad: Bool, + file: TelegramMediaFile, + cache: AnimationCache, + renderer: MultiAnimationRenderer, + unique: Bool = false, + placeholderColor: UIColor, + pointSize: CGSize, + dynamicColor: UIColor? = nil + ) { + self.init( + context: .account(context), + userLocation: userLocation, + attemptSynchronousLoad: attemptSynchronousLoad, + file: file, + cache: cache, + renderer: renderer, + unique: unique, + placeholderColor: placeholderColor, + pointSize: pointSize, + dynamicColor: dynamicColor + ) + } + + public init( + context: InlineFileIconLayer.Context, + userLocation: MediaResourceUserLocation, + attemptSynchronousLoad: Bool, + file: TelegramMediaFile, + cache: AnimationCache, + renderer: MultiAnimationRenderer, + unique: Bool = false, + placeholderColor: UIColor, + pointSize: CGSize, + dynamicColor: UIColor? = nil + ) { + let scale = min(2.0, UIScreenScale) + + self.arguments = Arguments( + context: context, + userLocation: userLocation, + file: file, + cache: cache, + renderer: renderer, + unique: unique, + placeholderColor: placeholderColor, + pointSize: pointSize, + pixelSize: CGSize(width: pointSize.width * scale, height: pointSize.height * scale) + ) + + self._dynamicColor = dynamicColor + + super.init() + + self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad) + } + + override public init(layer: Any) { + self.arguments = nil + + super.init(layer: layer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.loadDisposable?.dispose() + self.infoDisposable?.dispose() + self.disposable?.dispose() + self.fetchDisposable?.dispose() + } + + override public func action(forKey event: String) -> CAAction? { + if event == kCAOnOrderIn { + self.isInHierarchyValue = true + } else if event == kCAOnOrderOut { + self.isInHierarchyValue = false + } + return nullAction + } + + private func updateFile(file: TelegramMediaFile, attemptSynchronousLoad: Bool) { + guard let arguments = self.arguments else { + return + } + + if self.file?.fileId == file.fileId { + return + } + + self.file = file + + if attemptSynchronousLoad { + if !arguments.renderer.loadFirstFrameSynchronously(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, size: arguments.pixelSize) { + if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: arguments.pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: arguments.placeholderColor) { + self.contents = image.cgImage + self.isDisplayingPlaceholder = true + } + } + + self.loadAnimation() + } else { + let isTemplate = file.isCustomTemplateEmoji + + let pointSize = arguments.pointSize + let placeholderColor = arguments.placeholderColor + let isThumbnailCancelled = Atomic(value: false) + self.loadDisposable = arguments.renderer.loadFirstFrame( + target: self, + cache: arguments.cache, + itemId: file.resource.id.stringRepresentation, + size: arguments.pixelSize, + fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true, customColor: isTemplate ? .white : nil), completion: { [weak self] result, isFinal in + if !result { + MultiAnimationRendererImpl.firstFrameQueue.async { + let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) + + DispatchQueue.main.async { + guard let strongSelf = self, !isThumbnailCancelled.with({ $0 }) else { + return + } + if let image = image { + strongSelf.contents = image.cgImage + strongSelf.isDisplayingPlaceholder = true + } + + if isFinal { + strongSelf.loadAnimation() + } + } + } + } else { + guard let strongSelf = self else { + return + } + let _ = isThumbnailCancelled.swap(true) + strongSelf.loadAnimation() + } + }) + } + } + + private func loadAnimation() { + /*guard let arguments = self.arguments else { + return + } + + guard let file = self.file else { + return + } + + let isTemplate = file.isCustomTemplateEmoji + + let context = arguments.context + if file.isAnimatedSticker || file.isVideoSticker || file.isVideoEmoji { + let keyframeOnly = arguments.pixelSize.width >= 120.0 + + self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: animationCacheFetchFile(postbox: arguments.context.postbox, userLocation: arguments.userLocation, userContentType: .sticker, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: keyframeOnly, customColor: isTemplate ? .white : nil)) + } else { + self.disposable = arguments.renderer.add(target: self, cache: arguments.cache, itemId: file.resource.id.stringRepresentation, unique: arguments.unique, size: arguments.pixelSize, fetch: { options in + let dataDisposable = context.postbox.mediaBox.resourceData(file.resource).start(next: { result in + guard result.complete else { + return + } + + cacheStillSticker(path: result.path, width: Int(options.size.width), height: Int(options.size.height), writer: options.writer, customColor: isTemplate ? .white : nil) + }) + + let fetchDisposable = freeMediaFileResourceInteractiveFetched(postbox: context.postbox, userLocation: arguments.userLocation, fileReference: .customEmoji(media: file), resource: file.resource).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() + } + }) + }*/ + } + + override public func updateDisplayPlaceholder(displayPlaceholder: Bool) { + if self.isDisplayingPlaceholder == displayPlaceholder { + return + } + self.isDisplayingPlaceholder = displayPlaceholder + } + + override public func transitionToContents(_ contents: AnyObject, didLoop: Bool) { + if self.isDisplayingPlaceholder { + self.isDisplayingPlaceholder = false + + if let current = self.contents { + let previousLayer = SimpleLayer() + previousLayer.contents = current + previousLayer.frame = self.frame + self.superlayer?.insertSublayer(previousLayer, below: self) + previousLayer.opacity = 0.0 + previousLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak previousLayer] _ in + previousLayer?.removeFromSuperlayer() + }) + + self.contents = contents + self.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } else { + self.contents = contents + self.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } else { + self.contents = contents + } + } +} diff --git a/submodules/TelegramUI/Components/LottieCpp/BUILD b/submodules/TelegramUI/Components/LottieCpp/BUILD index 50f747cca6..9743934e12 100644 --- a/submodules/TelegramUI/Components/LottieCpp/BUILD +++ b/submodules/TelegramUI/Components/LottieCpp/BUILD @@ -15,7 +15,6 @@ objc_library( copts = [ "-Werror", "-I{}/Sources".format(package_name()), - "-O2", ], hdrs = glob([ "PublicHeaders/**/*.h", @@ -32,3 +31,18 @@ objc_library( "//visibility:public", ], ) + +cc_library( + name = "LottieCppBinding", + srcs = [], + hdrs = glob([ + "PublicHeaders/**/*.h", + ]), + includes = [ + "PublicHeaders", + ], + copts = [], + visibility = ["//visibility:public"], + linkstatic = 1, + tags = ["swift_module=LottieCppBinding"], +) diff --git a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimationContainer.h b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimationContainer.h index 7d99bda20e..f92e613cee 100644 --- a/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimationContainer.h +++ b/submodules/TelegramUI/Components/LottieCpp/PublicHeaders/LottieCpp/LottieAnimationContainer.h @@ -21,6 +21,30 @@ class RenderTreeNode; extern "C" { #endif +typedef struct { + CGRect bounds; + CGPoint position; + CATransform3D transform; + double opacity; + bool masksToBounds; + bool isHidden; +} LottieRenderNodeLayerData; + +typedef struct { + int64_t internalId; + bool isValid; + LottieRenderNodeLayerData layer; + CGRect globalRect; + CGRect localRect; + CATransform3D globalTransform; + bool drawsContent; + bool hasSimpleContents; + int drawContentDescendants; + bool isInvertedMatte; + int64_t maskId; + int subnodeCount; +} LottieRenderNodeProxy; + @interface LottieAnimationContainer : NSObject @property (nonatomic, strong, readonly) LottieAnimation * _Nonnull animation; @@ -34,6 +58,10 @@ extern "C" { - (std::shared_ptr)internalGetRootRenderTreeNode; #endif +- (int64_t)getRootRenderNodeProxy; +- (LottieRenderNodeProxy)getRenderNodeProxyById:(int64_t)nodeId __attribute__((objc_direct)); +- (LottieRenderNodeProxy)getRenderNodeSubnodeProxyById:(int64_t)nodeId index:(int)index __attribute__((objc_direct)); + @end #ifdef __cplusplus diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.cpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.cpp index f03c7f15ba..6e91c29572 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.cpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.cpp @@ -10,19 +10,10 @@ void batchInterpolate(std::vector const &from, std::vector::interpolate(fromVertex, toVertex, amount); - - resultPath.updateVertex(vertex, i, false); - } - } + static_assert(sizeof(PathElement) == 8 * 2 * 3); + + resultPath.setElementCount(elementCount); + vDSP_vintbD((double *)&from[0], 1, (double *)&to[0], 1, &amount, (double *)&resultPath.elements()[0], 1, elementCount * 2 * 3); } } diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.hpp b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.hpp index 84cec6a14c..d3c098a6a3 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.hpp +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/Lottie/Public/Keyframes/ValueInterpolators.hpp @@ -178,15 +178,6 @@ public: } } else { batchInterpolate(value.elements(), to.elements(), resultPath, amount); - - /*for (int i = 0; i < elementCount; i++) { - const auto &fromVertex = value.elements()[i].vertex; - const auto &toVertex = to.elements()[i].vertex; - - auto vertex = ValueInterpolator::interpolate(fromVertex, toVertex, amount); - - resultPath.updateVertex(vertex, i, false); - }*/ } } }; diff --git a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm index 14c6a227cb..a1b4a8dbeb 100644 --- a/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm +++ b/submodules/TelegramUI/Components/LottieCpp/Sources/LottieAnimationContainer.mm @@ -4,6 +4,7 @@ #include "LottieAnimationInternal.h" #include "RenderNode.hpp" #include "LottieRenderTreeInternal.h" +#include namespace lottie { @@ -367,6 +368,48 @@ static std::shared_ptr convertRenderTree(std::shared_ptr renderNode = [self internalGetRootRenderTreeNode]; + return (int64_t)renderNode.get(); +} + +- (LottieRenderNodeProxy)getRenderNodeProxyById:(int64_t)nodeId __attribute__((objc_direct)) { + lottie::RenderTreeNode *node = (lottie::RenderTreeNode *)nodeId; + + LottieRenderNodeProxy result; + + result.internalId = nodeId; + result.isValid = node->renderData.isValid; + + result.layer.bounds = CGRectMake(node->renderData.layer._bounds.x, node->renderData.layer._bounds.y, node->renderData.layer._bounds.width, node->renderData.layer._bounds.height); + result.layer.position = CGPointMake(node->renderData.layer._position.x, node->renderData.layer._position.y); + result.layer.transform = lottie::nativeTransform(node->renderData.layer._transform); + result.layer.opacity = node->renderData.layer._opacity; + result.layer.masksToBounds = node->renderData.layer._masksToBounds; + result.layer.isHidden = node->renderData.layer._isHidden; + + result.globalRect = CGRectMake(node->renderData.globalRect.x, node->renderData.globalRect.y, node->renderData.globalRect.width, node->renderData.globalRect.height); + result.localRect = CGRectMake(node->renderData.localRect.x, node->renderData.localRect.y, node->renderData.localRect.width, node->renderData.localRect.height); + result.globalTransform = lottie::nativeTransform(node->renderData.globalTransform); + result.drawsContent = node->renderData.drawsContent; + result.hasSimpleContents = node->renderData.drawContentDescendants <= 1; + result.drawContentDescendants = node->renderData.drawContentDescendants; + result.isInvertedMatte = node->renderData.isInvertedMatte; + if (node->mask()) { + result.maskId = (int64_t)node->mask().get(); + } else { + result.maskId = 0; + } + result.subnodeCount = (int)node->subnodes().size(); + + return result; +} + +- (LottieRenderNodeProxy)getRenderNodeSubnodeProxyById:(int64_t)nodeId index:(int)index __attribute__((objc_direct)) { + lottie::RenderTreeNode *node = (lottie::RenderTreeNode *)nodeId; + return [self getRenderNodeProxyById:(int64_t)node->subnodes()[index].get()]; +} + @end @implementation LottieAnimationContainer (Internal) diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift index ab5250460c..8b4ea6042e 100644 --- a/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift +++ b/submodules/TelegramUI/Components/LottieMetal/Sources/LottieMetalAnimatedStickerNode.swift @@ -52,6 +52,59 @@ private func generateTexture(device: MTLDevice, sideSize: Int, msaaSampleCount: return device.makeTexture(descriptor: textureDescriptor)! } +public func cacheLottieMetalAnimation(path: String) -> Data? { + if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data + if let lottieAnimation = LottieAnimation(data: decompressedData) { + let animationContainer = LottieAnimationContainer(animation: lottieAnimation) + + let startTime = CFAbsoluteTimeGetCurrent() + + let buffer = WriteBuffer() + var frameMapping = SerializedLottieMetalFrameMapping() + frameMapping.size = animationContainer.animation.size + frameMapping.frameCount = animationContainer.animation.frameCount + frameMapping.framesPerSecond = animationContainer.animation.framesPerSecond + for i in 0 ..< frameMapping.frameCount { + frameMapping.frameRanges[i] = 0 ..< 1 + } + serializeFrameMapping(buffer: buffer, frameMapping: frameMapping) + + for i in 0 ..< animationContainer.animation.frameCount { + animationContainer.update(i) + let frameRangeStart = buffer.length + if let node = animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0)) { + serializeNode(buffer: buffer, node: node) + let frameRangeEnd = buffer.length + frameMapping.frameRanges[i] = frameRangeStart ..< frameRangeEnd + } + } + + let previousLength = buffer.length + buffer.length = 0 + serializeFrameMapping(buffer: buffer, frameMapping: frameMapping) + buffer.length = previousLength + + buffer.trim() + let deltaTime = (CFAbsoluteTimeGetCurrent() - startTime) + let zippedData = TGGZipData(buffer.data, 1.0) + print("Serialized in \(deltaTime * 1000.0) size: \(zippedData.count / (1 * 1024 * 1024)) MB") + + return zippedData + } + } + return nil +} + +public func parseCachedLottieMetalAnimation(data: Data) -> LottieContentLayer.Content? { + if let unzippedData = TGGUnzipData(data, 32 * 1024 * 1024) { + let SerializedLottieMetalFrameMapping = deserializeFrameMapping(buffer: ReadBuffer(data: unzippedData)) + let serializedFrames = (SerializedLottieMetalFrameMapping, unzippedData) + return .serialized(frameMapping: serializedFrames.0, data: serializedFrames.1) + } + return nil +} + private final class AnimationCacheState { static let shared = AnimationCacheState() @@ -118,45 +171,8 @@ private final class AnimationCacheState { let cachePath = task.cachePath let queue = self.queue Queue.concurrentDefaultQueue().async { [weak self, weak task] in - if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { - let decompressedData = TGGUnzipData(data, 8 * 1024 * 1024) ?? data - if let lottieAnimation = LottieAnimation(data: decompressedData) { - let animationContainer = LottieAnimationContainer(animation: lottieAnimation) - - let startTime = CFAbsoluteTimeGetCurrent() - - let buffer = WriteBuffer() - var frameMapping = SerializedFrameMapping() - frameMapping.size = animationContainer.animation.size - frameMapping.frameCount = animationContainer.animation.frameCount - frameMapping.framesPerSecond = animationContainer.animation.framesPerSecond - for i in 0 ..< frameMapping.frameCount { - frameMapping.frameRanges[i] = 0 ..< 1 - } - serializeFrameMapping(buffer: buffer, frameMapping: frameMapping) - - for i in 0 ..< animationContainer.animation.frameCount { - animationContainer.update(i) - let frameRangeStart = buffer.length - if let node = animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0)) { - serializeNode(buffer: buffer, node: node) - let frameRangeEnd = buffer.length - frameMapping.frameRanges[i] = frameRangeStart ..< frameRangeEnd - } - } - - let previousLength = buffer.length - buffer.length = 0 - serializeFrameMapping(buffer: buffer, frameMapping: frameMapping) - buffer.length = previousLength - - buffer.trim() - let deltaTime = (CFAbsoluteTimeGetCurrent() - startTime) - let zippedData = TGGZipData(buffer.data, 1.0) - print("Serialized in \(deltaTime * 1000.0) size: \(zippedData.count / (1 * 1024 * 1024)) MB") - - let _ = try? zippedData.write(to: URL(fileURLWithPath: cachePath), options: .atomic) - } + if let zippedData = cacheLottieMetalAnimation(path: path) { + let _ = try? zippedData.write(to: URL(fileURLWithPath: cachePath), options: .atomic) } queue.async { @@ -191,12 +207,371 @@ private final class AnimationCacheState { } } +private func defaultTransformForSize(_ size: CGSize) -> CATransform3D { + var transform = CATransform3DIdentity + transform = CATransform3DScale(transform, 2.0 / size.width, 2.0 / size.height, 1.0) + transform = CATransform3DTranslate(transform, -size.width * 0.5, -size.height * 0.5, 0.0) + transform = CATransform3DTranslate(transform, 0.0, size.height, 0.0) + transform = CATransform3DScale(transform, 1.0, -1.0, 1.0) + + return transform +} + +private final class RenderFrameState { + let canvasSize: CGSize + let frameState: PathFrameState + let currentBezierIndicesBuffer: PathRenderBuffer + let currentBuffer: PathRenderBuffer + + var transform: CATransform3D + + init( + canvasSize: CGSize, + frameState: PathFrameState, + currentBezierIndicesBuffer: PathRenderBuffer, + currentBuffer: PathRenderBuffer + ) { + self.canvasSize = canvasSize + self.frameState = frameState + self.currentBezierIndicesBuffer = currentBezierIndicesBuffer + self.currentBuffer = currentBuffer + + self.transform = defaultTransformForSize(canvasSize) + } + + var transformStack: [CATransform3D] = [] + + func saveState() { + transformStack.append(transform) + } + + func restoreState() { + transform = transformStack.removeLast() + } + + func concat(_ other: CATransform3D) { + transform = CATransform3DConcat(other, transform) + } + + private func fillPath(path: LottiePath, shading: PathShading, rule: LottieFillRule, transform: CATransform3D) { + let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: rule, shading: shading, transform: transform) + + path.enumerateItems { pathItem in + switch pathItem.pointee.type { + case .moveTo: + let point = pathItem.pointee.points.0 + fillState.begin(point: SIMD2(Float(point.x), Float(point.y))) + case .lineTo: + let point = pathItem.pointee.points.0 + fillState.addLine(to: SIMD2(Float(point.x), Float(point.y))) + case .curveTo: + let cp1 = pathItem.pointee.points.0 + let cp2 = pathItem.pointee.points.1 + let point = pathItem.pointee.points.2 + + fillState.addCurve( + to: SIMD2(Float(point.x), Float(point.y)), + cp1: SIMD2(Float(cp1.x), Float(cp1.y)), + cp2: SIMD2(Float(cp2.x), Float(cp2.y)) + ) + case .close: + fillState.close() + @unknown default: + break + } + } + + fillState.close() + + self.frameState.add(fill: fillState) + } + + private func strokePath(path: LottiePath, width: CGFloat, join: CGLineJoin, cap: CGLineCap, miterLimit: CGFloat, color: LottieColor, transform: CATransform3D) { + let strokeState = PathRenderStrokeState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, lineWidth: Float(width), lineJoin: join, lineCap: cap, miterLimit: Float(miterLimit), color: color, transform: transform) + + path.enumerateItems { pathItem in + switch pathItem.pointee.type { + case .moveTo: + let point = pathItem.pointee.points.0 + strokeState.begin(point: SIMD2(Float(point.x), Float(point.y))) + case .lineTo: + let point = pathItem.pointee.points.0 + strokeState.addLine(to: SIMD2(Float(point.x), Float(point.y))) + case .curveTo: + let cp1 = pathItem.pointee.points.0 + let cp2 = pathItem.pointee.points.1 + let point = pathItem.pointee.points.2 + + strokeState.addCurve( + to: SIMD2(Float(point.x), Float(point.y)), + cp1: SIMD2(Float(cp1.x), Float(cp1.y)), + cp2: SIMD2(Float(cp2.x), Float(cp2.y)) + ) + case .close: + strokeState.close() + @unknown default: + break + } + } + + strokeState.complete() + + self.frameState.add(stroke: strokeState) + } + + func renderNodeContent(item: LottieRenderContent, alpha: Double) { + if let fill = item.fill { + if let solidShading = fill.shading as? LottieRenderContentSolidShading { + self.fillPath( + path: item.path, + shading: .color(LottieColor(r: solidShading.color.r, g: solidShading.color.g, b: solidShading.color.b, a: solidShading.color.a * solidShading.opacity * alpha)), + rule: fill.fillRule, + transform: transform + ) + } else if let gradientShading = fill.shading as? LottieRenderContentGradientShading { + let gradientType: PathShading.Gradient.GradientType + switch gradientShading.gradientType { + case .linear: + gradientType = .linear + case .radial: + gradientType = .radial + @unknown default: + gradientType = .linear + } + var colorStops: [PathShading.Gradient.ColorStop] = [] + for colorStop in gradientShading.colorStops { + colorStops.append(PathShading.Gradient.ColorStop( + color: LottieColor(r: colorStop.color.r, g: colorStop.color.g, b: colorStop.color.b, a: colorStop.color.a * gradientShading.opacity * alpha), + location: Float(colorStop.location) + )) + } + let gradientShading = PathShading.Gradient( + gradientType: gradientType, + colorStops: colorStops, + start: SIMD2(Float(gradientShading.start.x), Float(gradientShading.start.y)), + end: SIMD2(Float(gradientShading.end.x), Float(gradientShading.end.y)) + ) + self.fillPath( + path: item.path, + shading: .gradient(gradientShading), + rule: fill.fillRule, + transform: transform + ) + } + } else if let stroke = item.stroke { + if let solidShading = stroke.shading as? LottieRenderContentSolidShading { + let color = solidShading.color + strokePath( + path: item.path, + width: stroke.lineWidth, + join: stroke.lineJoin, + cap: stroke.lineCap, + miterLimit: stroke.miterLimit, + color: LottieColor(r: color.r, g: color.g, b: color.b, a: color.a * solidShading.opacity * alpha), + transform: transform + ) + } + } + } + + func renderNode(node: LottieRenderNode, globalSize: CGSize, parentAlpha: CGFloat) { + let normalizedOpacity = node.opacity + let layerAlpha = normalizedOpacity * parentAlpha + + if node.isHidden || normalizedOpacity == 0.0 { + return + } + + saveState() + + var needsTempContext = false + if node.mask != nil { + needsTempContext = true + } else { + needsTempContext = (layerAlpha != 1.0 && !node.hasSimpleContents) || node.masksToBounds + } + + var maskSurface: PathFrameState.MaskSurface? + + if needsTempContext { + if node.mask != nil || node.masksToBounds { + var maskMode: PathFrameState.MaskSurface.Mode = .regular + + frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height)) + saveState() + + transform = defaultTransformForSize(node.globalRect.size) + concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0)) + concat(node.globalTransform) + + if node.masksToBounds { + let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: .evenOdd, shading: .color(.init(r: 1.0, g: 1.0, b: 1.0, a: 1.0)), transform: transform) + + fillState.begin(point: SIMD2(Float(node.bounds.minX), Float(node.bounds.minY))) + fillState.addLine(to: SIMD2(Float(node.bounds.minX), Float(node.bounds.maxY))) + fillState.addLine(to: SIMD2(Float(node.bounds.maxX), Float(node.bounds.maxY))) + fillState.addLine(to: SIMD2(Float(node.bounds.maxX), Float(node.bounds.minY))) + fillState.close() + + frameState.add(fill: fillState) + } + if let maskNode = node.mask { + if maskNode.isInvertedMatte { + maskMode = .inverse + } + renderNode(node: maskNode, globalSize: globalSize, parentAlpha: 1.0) + } + + restoreState() + + maskSurface = frameState.popOffscreenMask(mode: maskMode) + } + + frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height)) + saveState() + + transform = defaultTransformForSize(node.globalRect.size) + concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0)) + concat(node.globalTransform) + } else { + concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0)) + concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0)) + concat(node.transform) + } + + var renderAlpha: CGFloat = 1.0 + if needsTempContext { + renderAlpha = 1.0 + } else { + renderAlpha = layerAlpha + } + + if let renderContent = node.renderContent { + renderNodeContent(item: renderContent, alpha: renderAlpha) + } + + for subnode in node.subnodes { + renderNode(node: subnode, globalSize: globalSize, parentAlpha: renderAlpha) + } + + if needsTempContext { + restoreState() + + concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0)) + concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0)) + concat(node.transform) + concat(CATransform3DInvert(node.globalTransform)) + + frameState.popOffscreen(rect: node.globalRect, transform: transform, opacity: Float(layerAlpha), mask: maskSurface) + } + + restoreState() + } + + func renderNode(animationContainer: LottieAnimationContainer, node: LottieRenderNodeProxy, globalSize: CGSize, parentAlpha: CGFloat) { + let normalizedOpacity = node.layer.opacity + let layerAlpha = normalizedOpacity * parentAlpha + + if node.layer.isHidden || normalizedOpacity == 0.0 { + return + } + + saveState() + + var needsTempContext = false + if node.maskId != 0 { + needsTempContext = true + } else { + needsTempContext = (layerAlpha != 1.0 && !node.hasSimpleContents) || node.layer.masksToBounds + } + + var maskSurface: PathFrameState.MaskSurface? + + if needsTempContext { + if node.maskId != 0 || node.layer.masksToBounds { + var maskMode: PathFrameState.MaskSurface.Mode = .regular + + frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height)) + saveState() + + transform = defaultTransformForSize(node.globalRect.size) + concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0)) + concat(node.globalTransform) + + if node.layer.masksToBounds { + let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: .evenOdd, shading: .color(.init(r: 1.0, g: 1.0, b: 1.0, a: 1.0)), transform: transform) + + fillState.begin(point: SIMD2(Float(node.layer.bounds.minX), Float(node.layer.bounds.minY))) + fillState.addLine(to: SIMD2(Float(node.layer.bounds.minX), Float(node.layer.bounds.maxY))) + fillState.addLine(to: SIMD2(Float(node.layer.bounds.maxX), Float(node.layer.bounds.maxY))) + fillState.addLine(to: SIMD2(Float(node.layer.bounds.maxX), Float(node.layer.bounds.minY))) + fillState.close() + + frameState.add(fill: fillState) + } + if node.maskId != 0 { + let maskNode = animationContainer.getRenderNodeProxy(byId: node.maskId) + if maskNode.isInvertedMatte { + maskMode = .inverse + } + renderNode(animationContainer: animationContainer, node: maskNode, globalSize: globalSize, parentAlpha: 1.0) + } + + restoreState() + + maskSurface = frameState.popOffscreenMask(mode: maskMode) + } + + frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height)) + saveState() + + transform = defaultTransformForSize(node.globalRect.size) + concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0)) + concat(node.globalTransform) + } else { + concat(CATransform3DMakeTranslation(node.layer.position.x, node.layer.position.y, 0.0)) + concat(CATransform3DMakeTranslation(-node.layer.bounds.origin.x, -node.layer.bounds.origin.y, 0.0)) + concat(node.layer.transform) + } + + var renderAlpha: CGFloat = 1.0 + if needsTempContext { + renderAlpha = 1.0 + } else { + renderAlpha = layerAlpha + } + + /*if let renderContent = node.renderContent { + renderNodeContent(item: renderContent, alpha: renderAlpha) + }*/ + assert(false) + + for i in 0 ..< node.subnodeCount { + let subnode = animationContainer.getRenderNodeSubnodeProxy(byId: node.internalId, index: i) + renderNode(animationContainer: animationContainer, node: subnode, globalSize: globalSize, parentAlpha: renderAlpha) + } + + if needsTempContext { + restoreState() + + concat(CATransform3DMakeTranslation(node.layer.position.x, node.layer.position.y, 0.0)) + concat(CATransform3DMakeTranslation(-node.layer.bounds.origin.x, -node.layer.bounds.origin.y, 0.0)) + concat(node.layer.transform) + concat(CATransform3DInvert(node.globalTransform)) + + frameState.popOffscreen(rect: node.globalRect, transform: transform, opacity: Float(layerAlpha), mask: maskSurface) + } + + restoreState() + } +} + public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubject { - enum Content { - case serialized(frameMapping: SerializedFrameMapping, data: Data) + public enum Content { + case serialized(frameMapping: SerializedLottieMetalFrameMapping, data: Data) case animation(LottieAnimationContainer) - var size: CGSize { + public var size: CGSize { switch self { case let .serialized(frameMapping, _): return frameMapping.size @@ -205,7 +580,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje } } - var frameCount: Int { + public var frameCount: Int { switch self { case let .serialized(frameMapping, _): return frameMapping.frameCount @@ -214,7 +589,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje } } - var framesPerSecond: Int { + public var framesPerSecond: Int { switch self { case let .serialized(frameMapping, _): return frameMapping.framesPerSecond @@ -288,7 +663,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje } } - init(content: Content) { + public init(content: Content) { self.content = content super.init() @@ -312,71 +687,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje fatalError("init(coder:) has not been implemented") } - private func fillPath(frameState: PathFrameState, path: LottiePath, shading: PathShading, rule: LottieFillRule, transform: CATransform3D) { - let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: rule, shading: shading, transform: transform) - - path.enumerateItems { pathItem in - switch pathItem.pointee.type { - case .moveTo: - let point = pathItem.pointee.points.0 - fillState.begin(point: SIMD2(Float(point.x), Float(point.y))) - case .lineTo: - let point = pathItem.pointee.points.0 - fillState.addLine(to: SIMD2(Float(point.x), Float(point.y))) - case .curveTo: - let cp1 = pathItem.pointee.points.0 - let cp2 = pathItem.pointee.points.1 - let point = pathItem.pointee.points.2 - - fillState.addCurve( - to: SIMD2(Float(point.x), Float(point.y)), - cp1: SIMD2(Float(cp1.x), Float(cp1.y)), - cp2: SIMD2(Float(cp2.x), Float(cp2.y)) - ) - case .close: - fillState.close() - @unknown default: - break - } - } - - fillState.close() - - frameState.add(fill: fillState) - } - - private func strokePath(frameState: PathFrameState, path: LottiePath, width: CGFloat, join: CGLineJoin, cap: CGLineCap, miterLimit: CGFloat, color: LottieColor, transform: CATransform3D) { - let strokeState = PathRenderStrokeState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, lineWidth: Float(width), lineJoin: join, lineCap: cap, miterLimit: Float(miterLimit), color: color, transform: transform) - - path.enumerateItems { pathItem in - switch pathItem.pointee.type { - case .moveTo: - let point = pathItem.pointee.points.0 - strokeState.begin(point: SIMD2(Float(point.x), Float(point.y))) - case .lineTo: - let point = pathItem.pointee.points.0 - strokeState.addLine(to: SIMD2(Float(point.x), Float(point.y))) - case .curveTo: - let cp1 = pathItem.pointee.points.0 - let cp2 = pathItem.pointee.points.1 - let point = pathItem.pointee.points.2 - - strokeState.addCurve( - to: SIMD2(Float(point.x), Float(point.y)), - cp1: SIMD2(Float(cp1.x), Float(cp1.y)), - cp2: SIMD2(Float(cp2.x), Float(cp2.y)) - ) - case .close: - strokeState.close() - @unknown default: - break - } - } - - strokeState.complete() - - frameState.add(stroke: strokeState) - } + private var renderNodeCache: [Int: LottieRenderNode] = [:] public func update(context: MetalEngineSubjectContext) { if self.bounds.isEmpty { @@ -392,196 +703,32 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje return } - guard let node = content.updateAndGetRenderNode(frameIndex: self.frameIndex) else { + var maybeNode: LottieRenderNode? + if let current = self.renderNodeCache[self.frameIndex] { + maybeNode = current + } else { + if let value = content.updateAndGetRenderNode(frameIndex: self.frameIndex) { + maybeNode = value + //self.renderNodeCache[self.frameIndex] = value + } + } + guard let node = maybeNode else { return } - func defaultTransformForSize(_ size: CGSize) -> CATransform3D { - var transform = CATransform3DIdentity - transform = CATransform3DScale(transform, 2.0 / size.width, 2.0 / size.height, 1.0) - transform = CATransform3DTranslate(transform, -size.width * 0.5, -size.height * 0.5, 0.0) - transform = CATransform3DTranslate(transform, 0.0, size.height, 0.0) - transform = CATransform3DScale(transform, 1.0, -1.0, 1.0) - - return transform - } - - let canvasSize = size - var transform = defaultTransformForSize(canvasSize) - - concat(CATransform3DMakeScale(canvasSize.width / content.size.width, canvasSize.height / content.size.height, 1.0)) - - var transformStack: [CATransform3D] = [] - - func saveState() { - transformStack.append(transform) - } - - func restoreState() { - transform = transformStack.removeLast() - } - - func concat(_ other: CATransform3D) { - transform = CATransform3DConcat(other, transform) - } - - func renderNodeContent(frameState: PathFrameState, item: LottieRenderContent, alpha: Double) { - if let fill = item.fill { - if let solidShading = fill.shading as? LottieRenderContentSolidShading { - self.fillPath( - frameState: frameState, - path: item.path, - shading: .color(LottieColor(r: solidShading.color.r, g: solidShading.color.g, b: solidShading.color.b, a: solidShading.color.a * solidShading.opacity * alpha)), - rule: fill.fillRule, - transform: transform - ) - } else if let gradientShading = fill.shading as? LottieRenderContentGradientShading { - let gradientType: PathShading.Gradient.GradientType - switch gradientShading.gradientType { - case .linear: - gradientType = .linear - case .radial: - gradientType = .radial - @unknown default: - gradientType = .linear - } - var colorStops: [PathShading.Gradient.ColorStop] = [] - for colorStop in gradientShading.colorStops { - colorStops.append(PathShading.Gradient.ColorStop( - color: LottieColor(r: colorStop.color.r, g: colorStop.color.g, b: colorStop.color.b, a: colorStop.color.a * gradientShading.opacity * alpha), - location: Float(colorStop.location) - )) - } - let gradientShading = PathShading.Gradient( - gradientType: gradientType, - colorStops: colorStops, - start: SIMD2(Float(gradientShading.start.x), Float(gradientShading.start.y)), - end: SIMD2(Float(gradientShading.end.x), Float(gradientShading.end.y)) - ) - self.fillPath( - frameState: frameState, - path: item.path, - shading: .gradient(gradientShading), - rule: fill.fillRule, - transform: transform - ) - } - } else if let stroke = item.stroke { - if let solidShading = stroke.shading as? LottieRenderContentSolidShading { - let color = solidShading.color - strokePath( - frameState: frameState, - path: item.path, - width: stroke.lineWidth, - join: stroke.lineJoin, - cap: stroke.lineCap, - miterLimit: stroke.miterLimit, - color: LottieColor(r: color.r, g: color.g, b: color.b, a: color.a * solidShading.opacity * alpha), - transform: transform - ) - } - } - } - - func renderNode(frameState: PathFrameState, node: LottieRenderNode, globalSize: CGSize, parentAlpha: CGFloat) { - let normalizedOpacity = node.opacity - let layerAlpha = normalizedOpacity * parentAlpha - - if node.isHidden || normalizedOpacity == 0.0 { - return - } - - saveState() - - var needsTempContext = false - if node.mask != nil { - needsTempContext = true - } else { - needsTempContext = (layerAlpha != 1.0 && !node.hasSimpleContents) || node.masksToBounds - } - - var maskSurface: PathFrameState.MaskSurface? - - if needsTempContext { - if node.mask != nil || node.masksToBounds { - var maskMode: PathFrameState.MaskSurface.Mode = .regular - - frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height)) - saveState() - - transform = defaultTransformForSize(node.globalRect.size) - concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0)) - concat(node.globalTransform) - - if node.masksToBounds { - let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: .evenOdd, shading: .color(.init(r: 1.0, g: 1.0, b: 1.0, a: 1.0)), transform: transform) - - fillState.begin(point: SIMD2(Float(node.bounds.minX), Float(node.bounds.minY))) - fillState.addLine(to: SIMD2(Float(node.bounds.minX), Float(node.bounds.maxY))) - fillState.addLine(to: SIMD2(Float(node.bounds.maxX), Float(node.bounds.maxY))) - fillState.addLine(to: SIMD2(Float(node.bounds.maxX), Float(node.bounds.minY))) - fillState.close() - - frameState.add(fill: fillState) - } - if let maskNode = node.mask { - if maskNode.isInvertedMatte { - maskMode = .inverse - } - renderNode(frameState: frameState, node: maskNode, globalSize: globalSize, parentAlpha: 1.0) - } - - restoreState() - - maskSurface = frameState.popOffscreenMask(mode: maskMode) - } - - frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height)) - saveState() - - transform = defaultTransformForSize(node.globalRect.size) - concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0)) - concat(node.globalTransform) - } else { - concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0)) - concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0)) - concat(node.transform) - } - - var renderAlpha: CGFloat = 1.0 - if needsTempContext { - renderAlpha = 1.0 - } else { - renderAlpha = layerAlpha - } - - if let renderContent = node.renderContent { - renderNodeContent(frameState: frameState, item: renderContent, alpha: renderAlpha) - } - - for subnode in node.subnodes { - renderNode(frameState: frameState, node: subnode, globalSize: globalSize, parentAlpha: renderAlpha) - } - - if needsTempContext { - restoreState() - - concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0)) - concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0)) - concat(node.transform) - concat(CATransform3DInvert(node.globalTransform)) - - frameState.popOffscreen(rect: node.globalRect, transform: transform, opacity: Float(layerAlpha), mask: maskSurface) - } - - restoreState() - } - self.currentBuffer.reset() self.currentBezierIndicesBuffer.reset() let frameState = PathFrameState(width: Int(size.width), height: Int(size.height), msaaSampleCount: self.msaaSampleCount, buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer) - renderNode(frameState: frameState, node: node, globalSize: canvasSize, parentAlpha: 1.0) + let frameContext = RenderFrameState( + canvasSize: size, + frameState: frameState, + currentBezierIndicesBuffer: self.currentBezierIndicesBuffer, + currentBuffer: self.currentBuffer + ) + frameContext.concat(CATransform3DMakeScale(frameContext.canvasSize.width / content.size.width, frameContext.canvasSize.height / content.size.height, 1.0)) + + frameContext.renderNode(node: node, globalSize: frameContext.canvasSize, parentAlpha: 1.0) final class ComputeOutput { let pathRenderContext: PathRenderContext @@ -693,7 +840,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje self.offscreenHeap = offscreenHeap } - frameState.encodeOffscreen(context: state.pathRenderContext, heap: offscreenHeap, commandBuffer: commandBuffer, canvasSize: canvasSize) + frameState.encodeOffscreen(context: state.pathRenderContext, heap: offscreenHeap, commandBuffer: commandBuffer, canvasSize: frameContext.canvasSize) guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { self.multisampleTextureQueue.append(multisampleTexture) @@ -701,7 +848,7 @@ public final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubje return nil } - frameState.encodeRender(context: state.pathRenderContext, encoder: renderEncoder, canvasSize: canvasSize) + frameState.encodeRender(context: state.pathRenderContext, encoder: renderEncoder, canvasSize: frameContext.canvasSize) renderEncoder.endEncoding() @@ -866,15 +1013,15 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke return } - var serializedFrames: (SerializedFrameMapping, Data)? + var serializedFrames: (SerializedLottieMetalFrameMapping, Data)? var cachePathValue: String? if let cachePathPrefix { let cachePath = cachePathPrefix + "-metal1" cachePathValue = cachePath if let data = try? Data(contentsOf: URL(fileURLWithPath: cachePath), options: .mappedIfSafe) { if let unzippedData = TGGUnzipData(data, 32 * 1024 * 1024) { - let serializedFrameMapping = deserializeFrameMapping(buffer: ReadBuffer(data: unzippedData)) - serializedFrames = (serializedFrameMapping, unzippedData) + let SerializedLottieMetalFrameMapping = deserializeFrameMapping(buffer: ReadBuffer(data: unzippedData)) + serializedFrames = (SerializedLottieMetalFrameMapping, unzippedData) } } } diff --git a/submodules/TelegramUI/Components/LottieMetal/Sources/RenderTreeSerialization.swift b/submodules/TelegramUI/Components/LottieMetal/Sources/RenderTreeSerialization.swift index 11d92645db..c52b176986 100644 --- a/submodules/TelegramUI/Components/LottieMetal/Sources/RenderTreeSerialization.swift +++ b/submodules/TelegramUI/Components/LottieMetal/Sources/RenderTreeSerialization.swift @@ -484,14 +484,14 @@ func deserializeNode(buffer: ReadBuffer) -> LottieRenderNode { ) } -struct SerializedFrameMapping { +public struct SerializedLottieMetalFrameMapping { var size: CGSize = CGSize() var frameCount: Int = 0 var framesPerSecond: Int = 0 var frameRanges: [Int: Range] = [:] } -func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedFrameMapping) { +func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedLottieMetalFrameMapping) { buffer.write(size: frameMapping.size) buffer.write(uInt32: UInt32(frameMapping.frameCount)) buffer.write(uInt32: UInt32(frameMapping.framesPerSecond)) @@ -502,8 +502,8 @@ func serializeFrameMapping(buffer: WriteBuffer, frameMapping: SerializedFrameMap } } -func deserializeFrameMapping(buffer: ReadBuffer) -> SerializedFrameMapping { - var frameMapping = SerializedFrameMapping() +func deserializeFrameMapping(buffer: ReadBuffer) -> SerializedLottieMetalFrameMapping { + var frameMapping = SerializedLottieMetalFrameMapping() frameMapping.size = buffer.readSize() frameMapping.frameCount = Int(buffer.readUInt32()) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index fb14d85e49..5ebfa32336 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -4450,7 +4450,7 @@ public final class StoryItemSetContainerComponent: Component { context: component.context, animationCache: component.context.animationCache, presentationData: component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme), - items: reactionItems.map(ReactionContextItem.reaction), + items: reactionItems.map { ReactionContextItem.reaction(item: $0, icon: .none) }, selectedItems: component.slice.item.storyItem.myReaction.flatMap { Set([$0]) } ?? Set(), title: self.displayLikeReactions ? nil : (isGroup ? component.strings.Story_SendReactionAsGroupMessage : component.strings.Story_SendReactionAsMessage), reactionsLocked: false, diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift index a5c4f4b52e..e2a4baa6a4 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerOpenMessageContextMenu.swift @@ -111,7 +111,7 @@ extension ChatControllerImpl { actions.animationCache = self.controllerInteraction?.presentationContext.animationCache if canAddMessageReactions(message: topMessage), let allowedReactions = allowedReactions, !topReactions.isEmpty { - actions.reactionItems = topReactions.map(ReactionContextItem.reaction) + actions.reactionItems = topReactions.map { ReactionContextItem.reaction(item: $0, icon: .none) } actions.selectedReactionItems = selectedReactions.reactions if message.areReactionsTags(accountPeerId: self.context.account.peerId) { if self.presentationInterfaceState.isPremium { @@ -131,7 +131,7 @@ extension ChatControllerImpl { if !actions.reactionItems.isEmpty { let reactionItems: [EmojiComponentReactionItem] = actions.reactionItems.compactMap { item -> EmojiComponentReactionItem? in switch item { - case let .reaction(reaction): + case let .reaction(reaction, _): return EmojiComponentReactionItem(reaction: reaction.reaction.rawValue, file: reaction.stillAnimation) default: return nil diff --git a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift index 82db6f7d73..28ae29b0c6 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatMessageDisplaySendMessageOptions.swift @@ -45,11 +45,22 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no effectItems = .single(nil) } + let availableMessageEffects = selfController.context.availableMessageEffects |> take(1) + let hasPremium = selfController.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: selfController.context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + let _ = (combineLatest( selfController.context.account.viewTracker.peerView(peerId) |> take(1), - effectItems + effectItems, + availableMessageEffects, + hasPremium ) - |> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView, effectItems in + |> deliverOnMainQueue).startStandalone(next: { [weak selfController] peerView, effectItems, availableMessageEffects, hasPremium in guard let selfController, let peer = peerViewMainPeer(peerView) else { return } @@ -98,7 +109,7 @@ func chatMessageDisplaySendMessageOptions(selfController: ChatControllerImpl, no return } selfController.controllerInteraction?.scheduleCurrentMessage() - }, reactionItems: effectItems) + }, reactionItems: effectItems, availableMessageEffects: availableMessageEffects, isPremium: hasPremium) selfController.sendMessageActionsController = controller if layout.isNonExclusive { selfController.present(controller, in: .window(.root)) diff --git a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift index f4de1aa775..0e69daa3c4 100644 --- a/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/WallpaperBackgroundNode.swift @@ -59,6 +59,8 @@ public protocol WallpaperBubbleBackgroundNode: ASDisplayNode { func update(rect: CGRect, within containerSize: CGSize, animator: ControlledTransitionAnimator) func offset(value: CGPoint, animationCurve: ContainedViewLayoutTransitionCurve, duration: Double) func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) + + func reloadBindings() } public enum WallpaperDisplayMode { @@ -323,7 +325,7 @@ private final class EffectImageLayer: SimpleLayer, GradientBackgroundPatternOver } } -final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode { +public final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode { final class BubbleBackgroundNodeImpl: ASDisplayNode, WallpaperBubbleBackgroundNode { var implicitContentUpdate: Bool = true @@ -631,6 +633,9 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode gradientWallpaperNode.layer.animateSpring(from: NSValue(cgPoint: scaledOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "contentsRect.position", duration: duration, initialVelocity: 0.0, damping: damping, additive: true) } } + + func reloadBindings() { + } } final class BubbleBackgroundPortalNodeImpl: ASDisplayNode, WallpaperBubbleBackgroundNode { @@ -679,6 +684,10 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode func offsetSpring(value: CGFloat, duration: Double, damping: CGFloat) { } + + func reloadBindings() { + self.portalView.reloadPortal() + } } private final class BubbleBackgroundNodeReference { @@ -812,7 +821,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode } } - var rotation: CGFloat = 0.0 { + public var rotation: CGFloat = 0.0 { didSet { var fromValue: CGFloat = 0.0 if let value = (self.layer.value(forKeyPath: "transform.rotation.z") as? NSNumber)?.floatValue { @@ -845,7 +854,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode private static var cachedSharedPattern: (PatternKey, UIImage)? private let _isReady = ValuePromise(false, ignoreRepeated: true) - var isReady: Signal { + public var isReady: Signal { return self._isReady.get() } @@ -920,7 +929,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode self.dimLayer.opacity = dimAlpha } - func update(wallpaper: TelegramWallpaper, animated: Bool) { + public func update(wallpaper: TelegramWallpaper, animated: Bool) { if self.wallpaper == wallpaper { return } @@ -1074,7 +1083,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode self.updateDimming() } - func _internalUpdateIsSettingUpWallpaper() { + public func _internalUpdateIsSettingUpWallpaper() { self.isSettingUpWallpaper = true } @@ -1301,7 +1310,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode transition.updateFrame(layer: self.patternImageLayer, frame: CGRect(origin: CGPoint(), size: size)) } - func updateLayout(size: CGSize, displayMode: WallpaperDisplayMode, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, displayMode: WallpaperDisplayMode, transition: ContainedViewLayoutTransition) { let isFirstLayout = self.validLayout == nil self.validLayout = (size, displayMode) @@ -1357,7 +1366,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode private var isAnimating = false private var isLooping = false - func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool) { + public func animateEvent(transition: ContainedViewLayoutTransition, extendAnimation: Bool) { guard !(self.isLooping && self.isAnimating) else { return } @@ -1373,7 +1382,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode self.outgoingBubbleGradientBackgroundNode?.animateEvent(transition: transition, extendAnimation: extendAnimation, backwards: false, completion: {}) } - func updateIsLooping(_ isLooping: Bool) { + public func updateIsLooping(_ isLooping: Bool) { let wasLooping = self.isLooping self.isLooping = isLooping @@ -1382,7 +1391,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode } } - func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) { + public func updateBubbleTheme(bubbleTheme: PresentationTheme, bubbleCorners: PresentationChatBubbleCorners) { if self.bubbleTheme !== bubbleTheme || self.bubbleCorners != bubbleCorners { self.bubbleTheme = bubbleTheme self.bubbleCorners = bubbleCorners @@ -1430,7 +1439,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode } } - func hasBubbleBackground(for type: WallpaperBubbleType) -> Bool { + public func hasBubbleBackground(for type: WallpaperBubbleType) -> Bool { guard let bubbleTheme = self.bubbleTheme, let bubbleCorners = self.bubbleCorners else { return false } @@ -1474,13 +1483,18 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode return false } + + public func makeLegacyBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? { + let node = WallpaperBackgroundNodeImpl.BubbleBackgroundNodeImpl(backgroundNode: self, bubbleType: type) + node.updateContents() + return node + } - func makeBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? { + public func makeBubbleBackground(for type: WallpaperBubbleType) -> WallpaperBubbleBackgroundNode? { if !self.hasBubbleBackground(for: type) { return nil } - #if true var sourceView: PortalSourceView? switch type { case .free: @@ -1499,14 +1513,9 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode let node = WallpaperBackgroundNodeImpl.BubbleBackgroundNodeImpl(backgroundNode: self, bubbleType: type) return node } - #else - let node = WallpaperBackgroundNodeImpl.BubbleBackgroundNodeImpl(backgroundNode: self, bubbleType: type) - node.updateContents() - return node - #endif } - func makeFreeBackground() -> PortalView? { + public func makeFreeBackground() -> PortalView? { if !self.hasBubbleBackground(for: .free) { return nil } @@ -1519,7 +1528,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode } } - func hasExtraBubbleBackground() -> Bool { + public func hasExtraBubbleBackground() -> Bool { var isInvertedGradient = false switch self.wallpaper { case let .file(file): @@ -1532,7 +1541,7 @@ final class WallpaperBackgroundNodeImpl: ASDisplayNode, WallpaperBackgroundNode return isInvertedGradient } - func makeDimmedNode() -> ASDisplayNode? { + public func makeDimmedNode() -> ASDisplayNode? { if let gradientBackgroundNode = self.gradientBackgroundNode { return GradientBackgroundNode.CloneNode(parentNode: gradientBackgroundNode) } else {