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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 {