From c141531c7be2365bd43df46ffc77c307077bd79b Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 19 Jul 2022 03:38:07 +0200 Subject: [PATCH] Animated emoji improvements --- .../Telegram-iOS/en.lproj/Localizable.strings | 2 + .../Sources/AnimatedStickerNode.swift | 4 +- .../Sources/DirectAnimatedStickerNode.swift | 2 + .../AttachmentTextInputPanelNode.swift | 2 +- .../Sources/ChatListController.swift | 33 +- .../Source/Base/Transition.swift | 19 + .../Sources/LottieAnimationComponent.swift | 75 +- .../Sources/PagerComponent.swift | 12 + .../ContextUI/Sources/PeekController.swift | 5 + .../Sources/PeekControllerContent.swift | 2 + .../Sources/PeekControllerNode.swift | 2 + .../Sources/StickerPreviewPeekContent.swift | 17 + .../Sources/StickerPreviewPeekContent.swift | 23 + submodules/TelegramApi/Sources/Api0.swift | 7 +- submodules/TelegramApi/Sources/Api15.swift | 136 +-- submodules/TelegramApi/Sources/Api16.swift | 180 ++-- submodules/TelegramApi/Sources/Api17.swift | 84 ++ submodules/TelegramApi/Sources/Api20.swift | 54 + submodules/TelegramApi/Sources/Api26.swift | 42 +- submodules/TelegramApi/Sources/Api28.swift | 23 +- .../Components/MediaStreamComponent.swift | 10 +- .../PendingMessages/EnqueueMessage.swift | 21 + .../Sources/State/AccountViewTracker.swift | 53 +- ...onizeInstalledStickerPacksOperations.swift | 10 +- .../Sources/State/StickerManagement.swift | 166 ++- ...nchronizeRecentlyUsedMediaOperations.swift | 3 + .../SyncCore/SyncCore_Namespaces.swift | 2 + .../SyncCore/SyncCore_RecentMediaItem.swift | 103 ++ .../TelegramEngine/Payments/AppStore.swift | 2 +- .../Payments/BotPaymentForm.swift | 5 +- .../Stickers/SearchStickers.swift | 2 +- .../TelegramEngine/Stickers/StickerPack.swift | 2 +- .../Stickers/StickerSetInstallation.swift | 107 +- .../Stickers/TelegramEngineStickers.swift | 7 + .../DefaultDarkPresentationTheme.swift | 2 +- .../Resources/PresentationResourcesChat.swift | 2 +- .../Sources/AnimationCache.swift | 918 ++++++++++------- .../AudioTranscriptionButtonComponent.swift | 26 +- ...anscriptionPendingIndicatorComponent.swift | 10 +- .../Sources/EmojiPagerContentComponent.swift | 957 ++++++++++++++---- .../Sources/EntityKeyboard.swift | 57 +- ...tyKeyboardTopContainerPanelComponent.swift | 12 +- .../EntityKeyboardTopPanelComponent.swift | 412 ++++++-- .../Sources/GifPagerContentComponent.swift | 8 +- .../Sources/LottieAnimationCache.swift | 22 +- .../Sources/MultiAnimationMetalRenderer.swift | 44 +- .../Sources/MultiAnimationRenderer.swift | 357 +++++-- .../Sources/TextNodeWithEntities.swift | 2 +- .../Sources/VideoAnimationCache.swift | 3 +- .../PanelBadgeAdd.imageset/Contents.json | 12 + .../PanelBadgeAdd.imageset/addstickers.pdf | 103 ++ .../PanelBadgeLock.imageset/Contents.json | 12 + .../lockedstickers.pdf | 93 ++ .../ChatContextResultPeekContentNode.swift | 4 + .../TelegramUI/Sources/ChatController.swift | 2 + .../Sources/ChatControllerNode.swift | 23 +- .../Sources/ChatEntityKeyboardInputNode.swift | 279 ++++- .../Sources/ChatTextInputPanelNode.swift | 2 +- 58 files changed, 3446 insertions(+), 1133 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeAdd.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeAdd.imageset/addstickers.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeLock.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeLock.imageset/lockedstickers.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index a044cdca39..2ec8127be1 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7849,3 +7849,5 @@ Sorry for the inconvenience."; "WebApp.CloseConfirmation" = "Changes that you made may not be saved."; "WebApp.CloseAnyway" = "Close Anyway"; + +"Emoji.ClearRecent" = "Clear Recent Emoji"; diff --git a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift index 99f320ca60..da854711f0 100644 --- a/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/AnimatedStickerNode.swift @@ -165,6 +165,7 @@ public protocol AnimatedStickerNode: ASDisplayNode { var autoplay: Bool { get set } var visibility: Bool { get set } + var overrideVisibility: Bool { get set } var isPlayingChanged: (Bool) -> Void { get } @@ -222,6 +223,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke } public var autoplay = false + public var overrideVisibility: Bool = false public var visibility = false { didSet { @@ -386,7 +388,7 @@ public final class DefaultAnimatedStickerNodeImpl: ASDisplayNode, AnimatedSticke private func updateIsPlaying() { if !self.autoplay { - let isPlaying = self.visibility && self.isDisplaying + let isPlaying = self.visibility && (self.isDisplaying || self.overrideVisibility) if self.isPlaying != isPlaying { self.isPlaying = isPlaying if isPlaying { diff --git a/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift b/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift index 76ed877028..9a36f9148d 100644 --- a/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift +++ b/submodules/AnimatedStickerNode/Sources/DirectAnimatedStickerNode.swift @@ -56,6 +56,8 @@ public final class DirectAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode } } + public var overrideVisibility: Bool = false + public var isPlayingChanged: (Bool) -> Void = { _ in } private var sourceDisposable: Disposable? diff --git a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift index 9d4a7ec421..925a02fd84 100644 --- a/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift +++ b/submodules/AttachmentTextInputPanelNode/Sources/AttachmentTextInputPanelNode.swift @@ -898,9 +898,9 @@ public class AttachmentTextInputPanelNode: ASDisplayNode, TGCaptionPanelView, AS let animationComponent = LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "anim_smiletosticker", - colors: colors, mode: .animateTransitionFromPrevious ), + colors: colors, size: CGSize(width: 32.0, height: 32.0) ) let inputNodeSize = self.inputModeView.update( diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 539cf35828..02ed9d2d8d 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -661,6 +661,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } let animation: LottieAnimationComponent.AnimationItem? + let colors: [String: UIColor] let progressValue: Double? switch state { case let .downloading(progress): @@ -668,13 +669,13 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController animation = LottieAnimationComponent.AnimationItem( name: "anim_search_downloading", - colors: [ - "Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor, - "Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, - "Arrow2.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, - ], mode: .animating(loop: true) ) + colors = [ + "Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor, + "Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, + "Arrow2.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, + ] progressValue = progress strongSelf.clearUnseenDownloadsTimer?.invalidate() @@ -684,18 +685,18 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController animation = LottieAnimationComponent.AnimationItem( name: "anim_search_downloaded", - colors: [ - "Fill 2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, - "Mask1.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, - "Mask2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, - "Arrow3.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, - "Fill.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, - "Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor, - "Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, - "Arrow2.Union.Fill 1": strongSelf.presentationData.theme.rootController.navigationSearchBar.inputFillColor.blitOver(strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, alpha: 1.0), - ], mode: .animating(loop: false) ) + colors = [ + "Fill 2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, + "Mask1.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, + "Mask2.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, + "Arrow3.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, + "Fill.Ellipse 1.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, + "Oval.Ellipse 1.Stroke 1": strongSelf.presentationData.theme.list.itemAccentColor, + "Arrow1.Union.Fill 1": strongSelf.presentationData.theme.list.itemAccentColor, + "Arrow2.Union.Fill 1": strongSelf.presentationData.theme.rootController.navigationSearchBar.inputFillColor.blitOver(strongSelf.presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, alpha: 1.0), + ] progressValue = 1.0 if strongSelf.clearUnseenDownloadsTimer == nil { @@ -718,6 +719,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.hasDownloads = hasDownloadsValue animation = nil + colors = [:] progressValue = nil strongSelf.clearUnseenDownloadsTimer?.invalidate() @@ -728,6 +730,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let contentComponent = AnyComponent(ZStack([ AnyComponentWithIdentity(id: 0, component: AnyComponent(LottieAnimationComponent( animation: animation, + colors: colors, size: CGSize(width: 24.0, height: 24.0) ))), AnyComponentWithIdentity(id: 1, component: AnyComponent(ProgressIndicatorComponent( diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index c21d1afcf7..c2ca5bc861 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -458,6 +458,25 @@ public struct Transition { ) } } + + public func animateSublayerScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + completion?(true) + case let .curve(duration, curve): + view.layer.animate( + from: fromValue as NSNumber, + to: toValue as NSNumber, + keyPath: "sublayerTransform.scale", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: additive, + completion: completion + ) + } + } public func animateAlpha(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift index 8c773c16df..df3e9b90a5 100644 --- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift +++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift @@ -20,21 +20,21 @@ public final class LottieAnimationComponent: Component { public var name: String public var mode: Mode - public var colors: [String: UIColor] - public init(name: String, colors: [String: UIColor], mode: Mode) { + public init(name: String, mode: Mode) { self.name = name - self.colors = colors self.mode = mode } } public let animation: AnimationItem + public let colors: [String: UIColor] public let tag: AnyObject? public let size: CGSize? - public init(animation: AnimationItem, tag: AnyObject? = nil, size: CGSize?) { + public init(animation: AnimationItem, colors: [String: UIColor], tag: AnyObject? = nil, size: CGSize?) { self.animation = animation + self.colors = colors self.tag = tag self.size = size } @@ -42,6 +42,7 @@ public final class LottieAnimationComponent: Component { public func tagged(_ tag: AnyObject?) -> LottieAnimationComponent { return LottieAnimationComponent( animation: self.animation, + colors: self.colors, tag: tag, size: self.size ) @@ -51,6 +52,9 @@ public final class LottieAnimationComponent: Component { if lhs.animation != rhs.animation { return false } + if lhs.colors != rhs.colors { + return false + } if lhs.tag !== rhs.tag { return false } @@ -117,6 +121,11 @@ public final class LottieAnimationComponent: Component { func update(component: LottieAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize { var updatePlayback = false + var updateColors = false + + if let currentComponent = self.component, currentComponent.colors != component.colors { + updateColors = true + } if self.component?.animation != component.animation { if let animationView = self.animationView { @@ -158,15 +167,7 @@ public final class LottieAnimationComponent: Component { view.backgroundColor = .clear view.isOpaque = false - if let value = component.animation.colors["__allcolors__"] { - for keypath in view.allKeypaths(predicate: { $0.keys.last == "Color" }) { - view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath)) - } - } - - for (key, value) in component.animation.colors { - view.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color")) - } + updateColors = true self.animationView = view self.addSubview(view) @@ -176,6 +177,23 @@ public final class LottieAnimationComponent: Component { } } + self.component = component + + if updateColors, let animationView = self.animationView { + if let value = component.colors["__allcolors__"] { + for keypath in animationView.allKeypaths(predicate: { $0.keys.last == "Color" }) { + animationView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: keypath)) + } + } + + for (key, value) in component.colors { + if key == "__allcolors__" { + continue + } + animationView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color")) + } + } + var animationSize = CGSize() if let animationView = self.animationView, let animation = animationView.animation { animationSize = animation.size @@ -187,7 +205,36 @@ public final class LottieAnimationComponent: Component { let size = CGSize(width: min(animationSize.width, availableSize.width), height: min(animationSize.height, availableSize.height)) if let animationView = self.animationView { - animationView.frame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize) + let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - animationSize.width) / 2.0), y: floor((size.height - animationSize.height) / 2.0)), size: animationSize) + + if animationView.frame != animationFrame { + if !transition.animation.isImmediate && !animationView.frame.isEmpty && animationView.frame.size != animationFrame.size { + let previouosAnimationFrame = animationView.frame + + if let snapshotView = animationView.snapshotView(afterScreenUpdates: false) { + snapshotView.frame = previouosAnimationFrame + + animationView.superview?.insertSubview(snapshotView, belowSubview: animationView) + + transition.setPosition(view: snapshotView, position: CGPoint(x: animationFrame.midX, y: animationFrame.midY)) + snapshotView.bounds = CGRect(origin: CGPoint(), size: animationFrame.size) + let scaleFactor = previouosAnimationFrame.width / animationFrame.width + transition.animateScale(view: snapshotView, from: scaleFactor, to: 1.0) + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + } + + transition.setPosition(view: animationView, position: CGPoint(x: animationFrame.midX, y: animationFrame.midY)) + transition.setBounds(view: animationView, bounds: CGRect(origin: CGPoint(), size: animationFrame.size)) + transition.animateSublayerScale(view: animationView, from: previouosAnimationFrame.width / animationFrame.width, to: 1.0) + animationView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) + } else if animationView.frame.size == animationFrame.size { + transition.setFrame(view: animationView, frame: animationFrame) + } else { + animationView.frame = animationFrame + } + } if updatePlayback { if case .animating = component.animation.mode { diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index b3b0ce76ce..d6db0ca729 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -261,6 +261,10 @@ public final class PagerComponent View { diff --git a/submodules/ContextUI/Sources/PeekController.swift b/submodules/ContextUI/Sources/PeekController.swift index 2bacd9ad6f..615979d884 100644 --- a/submodules/ContextUI/Sources/PeekController.swift +++ b/submodules/ContextUI/Sources/PeekController.swift @@ -68,6 +68,11 @@ public final class PeekController: ViewController, ContextControllerProtocol { private var animatedIn = false + private let _ready = Promise() + override public var ready: Promise { + return self._ready + } + public init(presentationData: PresentationData, content: PeekControllerContent, sourceView: @escaping () -> (UIView, CGRect)?) { self.presentationData = presentationData self.content = content diff --git a/submodules/ContextUI/Sources/PeekControllerContent.swift b/submodules/ContextUI/Sources/PeekControllerContent.swift index ba9b23d524..f5b877fc12 100644 --- a/submodules/ContextUI/Sources/PeekControllerContent.swift +++ b/submodules/ContextUI/Sources/PeekControllerContent.swift @@ -2,6 +2,7 @@ import Foundation import UIKit import AsyncDisplayKit import Display +import SwiftSignalKit public enum PeekControllerContentPresentation { case contained @@ -26,6 +27,7 @@ public protocol PeekControllerContent { } public protocol PeekControllerContentNode { + func ready() -> Signal func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize } diff --git a/submodules/ContextUI/Sources/PeekControllerNode.swift b/submodules/ContextUI/Sources/PeekControllerNode.swift index d53a35f73a..af9503a098 100644 --- a/submodules/ContextUI/Sources/PeekControllerNode.swift +++ b/submodules/ContextUI/Sources/PeekControllerNode.swift @@ -126,6 +126,8 @@ final class PeekControllerNode: ViewControllerTracingNode { } self.hapticFeedback.prepareTap() + + controller.ready.set(self.contentNode.ready()) } deinit { diff --git a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift index 9ce887618f..ed757880c8 100644 --- a/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/ImportStickerPackUI/Sources/StickerPreviewPeekContent.swift @@ -64,6 +64,8 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController private var containerLayout: (ContainerViewLayout, CGFloat)? + private let _ready = Promise() + init(account: Account, item: ImportStickerPack.Sticker) { self.account = account self.item = item @@ -104,6 +106,21 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController } self.addSubnode(self.textNode) + + if let animationNode = self.animationNode { + animationNode.started = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf._ready.set(.single(true)) + } + } else { + self._ready.set(.single(true)) + } + } + + func ready() -> Signal { + return self._ready.get() } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift index 07dbc281d7..1eeef0f26a 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift @@ -101,6 +101,8 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC private var containerLayout: (ContainerViewLayout, CGFloat)? + private let _ready = Promise() + init(account: Account, item: StickerPreviewPeekItem) { self.account = account self.item = item @@ -117,6 +119,7 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC if item.file.isAnimatedSticker || item.file.isVideoSticker { let animationNode = DefaultAnimatedStickerNodeImpl() + animationNode.overrideVisibility = true self.animationNode = animationNode let dimensions = item.file.dimensions ?? PixelDimensions(width: 512, height: 512) @@ -166,12 +169,32 @@ public final class StickerPreviewPeekContentNode: ASDisplayNode, PeekControllerC if let additionalAnimationNode = self.additionalAnimationNode { self.addSubnode(additionalAnimationNode) } + + if let animationNode = self.animationNode { + animationNode.started = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf._ready.set(.single(true)) + } + } else { + self.imageNode.imageUpdated = { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf._ready.set(.single(true)) + } + } } deinit { self.effectDisposable.dispose() } + public func ready() -> Signal { + return self._ready.get() + } + public func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let boundingSize: CGSize if let _ = self.additionalAnimationNode { diff --git a/submodules/TelegramApi/Sources/Api0.swift b/submodules/TelegramApi/Sources/Api0.swift index 37050808eb..daf94bbb99 100644 --- a/submodules/TelegramApi/Sources/Api0.swift +++ b/submodules/TelegramApi/Sources/Api0.swift @@ -544,6 +544,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[982592842] = { return Api.PasswordKdfAlgo.parse_passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow($0) } dict[-732254058] = { return Api.PasswordKdfAlgo.parse_passwordKdfAlgoUnknown($0) } dict[-368917890] = { return Api.PaymentCharge.parse_paymentCharge($0) } + dict[-1996951013] = { return Api.PaymentFormMethod.parse_paymentFormMethod($0) } dict[-1868808300] = { return Api.PaymentRequestedInfo.parse_paymentRequestedInfo($0) } dict[-842892769] = { return Api.PaymentSavedCredentials.parse_paymentSavedCredentialsCard($0) } dict[-1566230754] = { return Api.Peer.parse_peerChannel($0) } @@ -710,6 +711,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[313694676] = { return Api.StickerPack.parse_stickerPack($0) } dict[768691932] = { return Api.StickerSet.parse_stickerSet($0) } dict[1678812626] = { return Api.StickerSetCovered.parse_stickerSetCovered($0) } + dict[451763941] = { return Api.StickerSetCovered.parse_stickerSetFullCovered($0) } dict[872932635] = { return Api.StickerSetCovered.parse_stickerSetMultiCovered($0) } dict[-1609668650] = { return Api.Theme.parse_theme($0) } dict[-94849324] = { return Api.ThemeSettings.parse_themeSettings($0) } @@ -805,6 +807,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[1767677564] = { return Api.Update.parse_updateReadChannelDiscussionOutbox($0) } dict[-1842450928] = { return Api.Update.parse_updateReadChannelInbox($0) } dict[-1218471511] = { return Api.Update.parse_updateReadChannelOutbox($0) } + dict[-78886548] = { return Api.Update.parse_updateReadFeaturedEmojiStickers($0) } dict[1461528386] = { return Api.Update.parse_updateReadFeaturedStickers($0) } dict[-1667805217] = { return Api.Update.parse_updateReadHistoryInbox($0) } dict[791617983] = { return Api.Update.parse_updateReadHistoryOutbox($0) } @@ -1003,7 +1006,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = { dict[136574537] = { return Api.messages.VotesList.parse_votesList($0) } dict[1042605427] = { return Api.payments.BankCardData.parse_bankCardData($0) } dict[-1362048039] = { return Api.payments.ExportedInvoice.parse_exportedInvoice($0) } - dict[-1340916937] = { return Api.payments.PaymentForm.parse_paymentForm($0) } + dict[1288001087] = { return Api.payments.PaymentForm.parse_paymentForm($0) } dict[1891958275] = { return Api.payments.PaymentReceipt.parse_paymentReceipt($0) } dict[1314881805] = { return Api.payments.PaymentResult.parse_paymentResult($0) } dict[-666824391] = { return Api.payments.PaymentResult.parse_paymentVerificationNeeded($0) } @@ -1418,6 +1421,8 @@ public extension Api { _1.serialize(buffer, boxed) case let _1 as Api.PaymentCharge: _1.serialize(buffer, boxed) + case let _1 as Api.PaymentFormMethod: + _1.serialize(buffer, boxed) case let _1 as Api.PaymentRequestedInfo: _1.serialize(buffer, boxed) case let _1 as Api.PaymentSavedCredentials: diff --git a/submodules/TelegramApi/Sources/Api15.swift b/submodules/TelegramApi/Sources/Api15.swift index d25de327ff..b797c99619 100644 --- a/submodules/TelegramApi/Sources/Api15.swift +++ b/submodules/TelegramApi/Sources/Api15.swift @@ -326,6 +326,46 @@ public extension Api { } } +public extension Api { + enum PaymentFormMethod: TypeConstructorDescription { + case paymentFormMethod(url: String, title: String) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .paymentFormMethod(let url, let title): + if boxed { + buffer.appendInt32(-1996951013) + } + serializeString(url, buffer: buffer, boxed: false) + serializeString(title, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .paymentFormMethod(let url, let title): + return ("paymentFormMethod", [("url", String(describing: url)), ("title", String(describing: title))]) + } + } + + public static func parse_paymentFormMethod(_ reader: BufferReader) -> PaymentFormMethod? { + var _1: String? + _1 = parseString(reader) + var _2: String? + _2 = parseString(reader) + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.PaymentFormMethod.paymentFormMethod(url: _1!, title: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum PaymentRequestedInfo: TypeConstructorDescription { case paymentRequestedInfo(flags: Int32, name: String?, phone: String?, email: String?, shippingAddress: Api.PostAddress?) @@ -1228,99 +1268,3 @@ public extension Api { } } -public extension Api { - enum Photo: TypeConstructorDescription { - case photo(flags: Int32, id: Int64, accessHash: Int64, fileReference: Buffer, date: Int32, sizes: [Api.PhotoSize], videoSizes: [Api.VideoSize]?, dcId: Int32) - case photoEmpty(id: Int64) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .photo(let flags, let id, let accessHash, let fileReference, let date, let sizes, let videoSizes, let dcId): - if boxed { - buffer.appendInt32(-82216347) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeInt64(id, buffer: buffer, boxed: false) - serializeInt64(accessHash, buffer: buffer, boxed: false) - serializeBytes(fileReference, buffer: buffer, boxed: false) - serializeInt32(date, buffer: buffer, boxed: false) - buffer.appendInt32(481674261) - buffer.appendInt32(Int32(sizes.count)) - for item in sizes { - item.serialize(buffer, true) - } - if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) - buffer.appendInt32(Int32(videoSizes!.count)) - for item in videoSizes! { - item.serialize(buffer, true) - }} - serializeInt32(dcId, buffer: buffer, boxed: false) - break - case .photoEmpty(let id): - if boxed { - buffer.appendInt32(590459437) - } - serializeInt64(id, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .photo(let flags, let id, let accessHash, let fileReference, let date, let sizes, let videoSizes, let dcId): - return ("photo", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("fileReference", String(describing: fileReference)), ("date", String(describing: date)), ("sizes", String(describing: sizes)), ("videoSizes", String(describing: videoSizes)), ("dcId", String(describing: dcId))]) - case .photoEmpty(let id): - return ("photoEmpty", [("id", String(describing: id))]) - } - } - - public static func parse_photo(_ reader: BufferReader) -> Photo? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int64? - _2 = reader.readInt64() - var _3: Int64? - _3 = reader.readInt64() - var _4: Buffer? - _4 = parseBytes(reader) - var _5: Int32? - _5 = reader.readInt32() - var _6: [Api.PhotoSize]? - if let _ = reader.readInt32() { - _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PhotoSize.self) - } - var _7: [Api.VideoSize]? - if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { - _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.VideoSize.self) - } } - var _8: Int32? - _8 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - let _c4 = _4 != nil - let _c5 = _5 != nil - let _c6 = _6 != nil - let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil - let _c8 = _8 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { - return Api.Photo.photo(flags: _1!, id: _2!, accessHash: _3!, fileReference: _4!, date: _5!, sizes: _6!, videoSizes: _7, dcId: _8!) - } - else { - return nil - } - } - public static func parse_photoEmpty(_ reader: BufferReader) -> Photo? { - var _1: Int64? - _1 = reader.readInt64() - let _c1 = _1 != nil - if _c1 { - return Api.Photo.photoEmpty(id: _1!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api16.swift b/submodules/TelegramApi/Sources/Api16.swift index 830291d176..2c3236c9e6 100644 --- a/submodules/TelegramApi/Sources/Api16.swift +++ b/submodules/TelegramApi/Sources/Api16.swift @@ -1,3 +1,99 @@ +public extension Api { + enum Photo: TypeConstructorDescription { + case photo(flags: Int32, id: Int64, accessHash: Int64, fileReference: Buffer, date: Int32, sizes: [Api.PhotoSize], videoSizes: [Api.VideoSize]?, dcId: Int32) + case photoEmpty(id: Int64) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .photo(let flags, let id, let accessHash, let fileReference, let date, let sizes, let videoSizes, let dcId): + if boxed { + buffer.appendInt32(-82216347) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeInt64(id, buffer: buffer, boxed: false) + serializeInt64(accessHash, buffer: buffer, boxed: false) + serializeBytes(fileReference, buffer: buffer, boxed: false) + serializeInt32(date, buffer: buffer, boxed: false) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(sizes.count)) + for item in sizes { + item.serialize(buffer, true) + } + if Int(flags) & Int(1 << 1) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(videoSizes!.count)) + for item in videoSizes! { + item.serialize(buffer, true) + }} + serializeInt32(dcId, buffer: buffer, boxed: false) + break + case .photoEmpty(let id): + if boxed { + buffer.appendInt32(590459437) + } + serializeInt64(id, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .photo(let flags, let id, let accessHash, let fileReference, let date, let sizes, let videoSizes, let dcId): + return ("photo", [("flags", String(describing: flags)), ("id", String(describing: id)), ("accessHash", String(describing: accessHash)), ("fileReference", String(describing: fileReference)), ("date", String(describing: date)), ("sizes", String(describing: sizes)), ("videoSizes", String(describing: videoSizes)), ("dcId", String(describing: dcId))]) + case .photoEmpty(let id): + return ("photoEmpty", [("id", String(describing: id))]) + } + } + + public static func parse_photo(_ reader: BufferReader) -> Photo? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int64? + _2 = reader.readInt64() + var _3: Int64? + _3 = reader.readInt64() + var _4: Buffer? + _4 = parseBytes(reader) + var _5: Int32? + _5 = reader.readInt32() + var _6: [Api.PhotoSize]? + if let _ = reader.readInt32() { + _6 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PhotoSize.self) + } + var _7: [Api.VideoSize]? + if Int(_1!) & Int(1 << 1) != 0 {if let _ = reader.readInt32() { + _7 = Api.parseVector(reader, elementSignature: 0, elementType: Api.VideoSize.self) + } } + var _8: Int32? + _8 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + let _c4 = _4 != nil + let _c5 = _5 != nil + let _c6 = _6 != nil + let _c7 = (Int(_1!) & Int(1 << 1) == 0) || _7 != nil + let _c8 = _8 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 { + return Api.Photo.photo(flags: _1!, id: _2!, accessHash: _3!, fileReference: _4!, date: _5!, sizes: _6!, videoSizes: _7, dcId: _8!) + } + else { + return nil + } + } + public static func parse_photoEmpty(_ reader: BufferReader) -> Photo? { + var _1: Int64? + _1 = reader.readInt64() + let _c1 = _1 != nil + if _c1 { + return Api.Photo.photoEmpty(id: _1!) + } + else { + return nil + } + } + + } +} public extension Api { enum PhotoSize: TypeConstructorDescription { case photoCachedSize(type: String, w: Int32, h: Int32, bytes: Buffer) @@ -848,87 +944,3 @@ public extension Api { } } -public extension Api { - enum ReactionCount: TypeConstructorDescription { - case reactionCount(flags: Int32, reaction: String, count: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .reactionCount(let flags, let reaction, let count): - if boxed { - buffer.appendInt32(1873957073) - } - serializeInt32(flags, buffer: buffer, boxed: false) - serializeString(reaction, buffer: buffer, boxed: false) - serializeInt32(count, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .reactionCount(let flags, let reaction, let count): - return ("reactionCount", [("flags", String(describing: flags)), ("reaction", String(describing: reaction)), ("count", String(describing: count))]) - } - } - - public static func parse_reactionCount(_ reader: BufferReader) -> ReactionCount? { - var _1: Int32? - _1 = reader.readInt32() - var _2: String? - _2 = parseString(reader) - var _3: Int32? - _3 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - let _c3 = _3 != nil - if _c1 && _c2 && _c3 { - return Api.ReactionCount.reactionCount(flags: _1!, reaction: _2!, count: _3!) - } - else { - return nil - } - } - - } -} -public extension Api { - enum ReceivedNotifyMessage: TypeConstructorDescription { - case receivedNotifyMessage(id: Int32, flags: Int32) - - public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { - switch self { - case .receivedNotifyMessage(let id, let flags): - if boxed { - buffer.appendInt32(-1551583367) - } - serializeInt32(id, buffer: buffer, boxed: false) - serializeInt32(flags, buffer: buffer, boxed: false) - break - } - } - - public func descriptionFields() -> (String, [(String, Any)]) { - switch self { - case .receivedNotifyMessage(let id, let flags): - return ("receivedNotifyMessage", [("id", String(describing: id)), ("flags", String(describing: flags))]) - } - } - - public static func parse_receivedNotifyMessage(_ reader: BufferReader) -> ReceivedNotifyMessage? { - var _1: Int32? - _1 = reader.readInt32() - var _2: Int32? - _2 = reader.readInt32() - let _c1 = _1 != nil - let _c2 = _2 != nil - if _c1 && _c2 { - return Api.ReceivedNotifyMessage.receivedNotifyMessage(id: _1!, flags: _2!) - } - else { - return nil - } - } - - } -} diff --git a/submodules/TelegramApi/Sources/Api17.swift b/submodules/TelegramApi/Sources/Api17.swift index 31892c8e6f..6fbc72af8f 100644 --- a/submodules/TelegramApi/Sources/Api17.swift +++ b/submodules/TelegramApi/Sources/Api17.swift @@ -1,3 +1,87 @@ +public extension Api { + enum ReactionCount: TypeConstructorDescription { + case reactionCount(flags: Int32, reaction: String, count: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .reactionCount(let flags, let reaction, let count): + if boxed { + buffer.appendInt32(1873957073) + } + serializeInt32(flags, buffer: buffer, boxed: false) + serializeString(reaction, buffer: buffer, boxed: false) + serializeInt32(count, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .reactionCount(let flags, let reaction, let count): + return ("reactionCount", [("flags", String(describing: flags)), ("reaction", String(describing: reaction)), ("count", String(describing: count))]) + } + } + + public static func parse_reactionCount(_ reader: BufferReader) -> ReactionCount? { + var _1: Int32? + _1 = reader.readInt32() + var _2: String? + _2 = parseString(reader) + var _3: Int32? + _3 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.ReactionCount.reactionCount(flags: _1!, reaction: _2!, count: _3!) + } + else { + return nil + } + } + + } +} +public extension Api { + enum ReceivedNotifyMessage: TypeConstructorDescription { + case receivedNotifyMessage(id: Int32, flags: Int32) + + public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { + switch self { + case .receivedNotifyMessage(let id, let flags): + if boxed { + buffer.appendInt32(-1551583367) + } + serializeInt32(id, buffer: buffer, boxed: false) + serializeInt32(flags, buffer: buffer, boxed: false) + break + } + } + + public func descriptionFields() -> (String, [(String, Any)]) { + switch self { + case .receivedNotifyMessage(let id, let flags): + return ("receivedNotifyMessage", [("id", String(describing: id)), ("flags", String(describing: flags))]) + } + } + + public static func parse_receivedNotifyMessage(_ reader: BufferReader) -> ReceivedNotifyMessage? { + var _1: Int32? + _1 = reader.readInt32() + var _2: Int32? + _2 = reader.readInt32() + let _c1 = _1 != nil + let _c2 = _2 != nil + if _c1 && _c2 { + return Api.ReceivedNotifyMessage.receivedNotifyMessage(id: _1!, flags: _2!) + } + else { + return nil + } + } + + } +} public extension Api { enum RecentMeUrl: TypeConstructorDescription { case recentMeUrlChat(url: String, chatId: Int64) diff --git a/submodules/TelegramApi/Sources/Api20.swift b/submodules/TelegramApi/Sources/Api20.swift index ca0fd02867..b6b4d1684b 100644 --- a/submodules/TelegramApi/Sources/Api20.swift +++ b/submodules/TelegramApi/Sources/Api20.swift @@ -87,6 +87,7 @@ public extension Api { public extension Api { enum StickerSetCovered: TypeConstructorDescription { case stickerSetCovered(set: Api.StickerSet, cover: Api.Document) + case stickerSetFullCovered(set: Api.StickerSet, packs: [Api.StickerPack], documents: [Api.Document]) case stickerSetMultiCovered(set: Api.StickerSet, covers: [Api.Document]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { @@ -98,6 +99,22 @@ public extension Api { set.serialize(buffer, true) cover.serialize(buffer, true) break + case .stickerSetFullCovered(let set, let packs, let documents): + if boxed { + buffer.appendInt32(451763941) + } + set.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(packs.count)) + for item in packs { + item.serialize(buffer, true) + } + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(documents.count)) + for item in documents { + item.serialize(buffer, true) + } + break case .stickerSetMultiCovered(let set, let covers): if boxed { buffer.appendInt32(872932635) @@ -116,6 +133,8 @@ public extension Api { switch self { case .stickerSetCovered(let set, let cover): return ("stickerSetCovered", [("set", String(describing: set)), ("cover", String(describing: cover))]) + case .stickerSetFullCovered(let set, let packs, let documents): + return ("stickerSetFullCovered", [("set", String(describing: set)), ("packs", String(describing: packs)), ("documents", String(describing: documents))]) case .stickerSetMultiCovered(let set, let covers): return ("stickerSetMultiCovered", [("set", String(describing: set)), ("covers", String(describing: covers))]) } @@ -139,6 +158,29 @@ public extension Api { return nil } } + public static func parse_stickerSetFullCovered(_ reader: BufferReader) -> StickerSetCovered? { + var _1: Api.StickerSet? + if let signature = reader.readInt32() { + _1 = Api.parse(reader, signature: signature) as? Api.StickerSet + } + var _2: [Api.StickerPack]? + if let _ = reader.readInt32() { + _2 = Api.parseVector(reader, elementSignature: 0, elementType: Api.StickerPack.self) + } + var _3: [Api.Document]? + if let _ = reader.readInt32() { + _3 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Document.self) + } + let _c1 = _1 != nil + let _c2 = _2 != nil + let _c3 = _3 != nil + if _c1 && _c2 && _c3 { + return Api.StickerSetCovered.stickerSetFullCovered(set: _1!, packs: _2!, documents: _3!) + } + else { + return nil + } + } public static func parse_stickerSetMultiCovered(_ reader: BufferReader) -> StickerSetCovered? { var _1: Api.StickerSet? if let signature = reader.readInt32() { @@ -592,6 +634,7 @@ public extension Api { case updateReadChannelDiscussionOutbox(channelId: Int64, topMsgId: Int32, readMaxId: Int32) case updateReadChannelInbox(flags: Int32, folderId: Int32?, channelId: Int64, maxId: Int32, stillUnreadCount: Int32, pts: Int32) case updateReadChannelOutbox(channelId: Int64, maxId: Int32) + case updateReadFeaturedEmojiStickers case updateReadFeaturedStickers case updateReadHistoryInbox(flags: Int32, folderId: Int32?, peer: Api.Peer, maxId: Int32, stillUnreadCount: Int32, pts: Int32, ptsCount: Int32) case updateReadHistoryOutbox(peer: Api.Peer, maxId: Int32, pts: Int32, ptsCount: Int32) @@ -1332,6 +1375,12 @@ public extension Api { } serializeInt64(channelId, buffer: buffer, boxed: false) serializeInt32(maxId, buffer: buffer, boxed: false) + break + case .updateReadFeaturedEmojiStickers: + if boxed { + buffer.appendInt32(-78886548) + } + break case .updateReadFeaturedStickers: if boxed { @@ -1660,6 +1709,8 @@ public extension Api { return ("updateReadChannelInbox", [("flags", String(describing: flags)), ("folderId", String(describing: folderId)), ("channelId", String(describing: channelId)), ("maxId", String(describing: maxId)), ("stillUnreadCount", String(describing: stillUnreadCount)), ("pts", String(describing: pts))]) case .updateReadChannelOutbox(let channelId, let maxId): return ("updateReadChannelOutbox", [("channelId", String(describing: channelId)), ("maxId", String(describing: maxId))]) + case .updateReadFeaturedEmojiStickers: + return ("updateReadFeaturedEmojiStickers", []) case .updateReadFeaturedStickers: return ("updateReadFeaturedStickers", []) case .updateReadHistoryInbox(let flags, let folderId, let peer, let maxId, let stillUnreadCount, let pts, let ptsCount): @@ -3196,6 +3247,9 @@ public extension Api { return nil } } + public static func parse_updateReadFeaturedEmojiStickers(_ reader: BufferReader) -> Update? { + return Api.Update.updateReadFeaturedEmojiStickers + } public static func parse_updateReadFeaturedStickers(_ reader: BufferReader) -> Update? { return Api.Update.updateReadFeaturedStickers } diff --git a/submodules/TelegramApi/Sources/Api26.swift b/submodules/TelegramApi/Sources/Api26.swift index 9dadf48c72..2d4247b260 100644 --- a/submodules/TelegramApi/Sources/Api26.swift +++ b/submodules/TelegramApi/Sources/Api26.swift @@ -728,13 +728,13 @@ public extension Api.payments { } public extension Api.payments { enum PaymentForm: TypeConstructorDescription { - case paymentForm(flags: Int32, formId: Int64, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: Api.PaymentSavedCredentials?, users: [Api.User]) + case paymentForm(flags: Int32, formId: Int64, botId: Int64, title: String, description: String, photo: Api.WebDocument?, invoice: Api.Invoice, providerId: Int64, url: String, nativeProvider: String?, nativeParams: Api.DataJSON?, additionalMethods: [Api.PaymentFormMethod]?, savedInfo: Api.PaymentRequestedInfo?, savedCredentials: Api.PaymentSavedCredentials?, users: [Api.User]) public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { switch self { - case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users): + case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let additionalMethods, let savedInfo, let savedCredentials, let users): if boxed { - buffer.appendInt32(-1340916937) + buffer.appendInt32(1288001087) } serializeInt32(flags, buffer: buffer, boxed: false) serializeInt64(formId, buffer: buffer, boxed: false) @@ -747,6 +747,11 @@ public extension Api.payments { serializeString(url, buffer: buffer, boxed: false) if Int(flags) & Int(1 << 4) != 0 {serializeString(nativeProvider!, buffer: buffer, boxed: false)} if Int(flags) & Int(1 << 4) != 0 {nativeParams!.serialize(buffer, true)} + if Int(flags) & Int(1 << 6) != 0 {buffer.appendInt32(481674261) + buffer.appendInt32(Int32(additionalMethods!.count)) + for item in additionalMethods! { + item.serialize(buffer, true) + }} if Int(flags) & Int(1 << 0) != 0 {savedInfo!.serialize(buffer, true)} if Int(flags) & Int(1 << 1) != 0 {savedCredentials!.serialize(buffer, true)} buffer.appendInt32(481674261) @@ -760,8 +765,8 @@ public extension Api.payments { public func descriptionFields() -> (String, [(String, Any)]) { switch self { - case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let savedInfo, let savedCredentials, let users): - return ("paymentForm", [("flags", String(describing: flags)), ("formId", String(describing: formId)), ("botId", String(describing: botId)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("invoice", String(describing: invoice)), ("providerId", String(describing: providerId)), ("url", String(describing: url)), ("nativeProvider", String(describing: nativeProvider)), ("nativeParams", String(describing: nativeParams)), ("savedInfo", String(describing: savedInfo)), ("savedCredentials", String(describing: savedCredentials)), ("users", String(describing: users))]) + case .paymentForm(let flags, let formId, let botId, let title, let description, let photo, let invoice, let providerId, let url, let nativeProvider, let nativeParams, let additionalMethods, let savedInfo, let savedCredentials, let users): + return ("paymentForm", [("flags", String(describing: flags)), ("formId", String(describing: formId)), ("botId", String(describing: botId)), ("title", String(describing: title)), ("description", String(describing: description)), ("photo", String(describing: photo)), ("invoice", String(describing: invoice)), ("providerId", String(describing: providerId)), ("url", String(describing: url)), ("nativeProvider", String(describing: nativeProvider)), ("nativeParams", String(describing: nativeParams)), ("additionalMethods", String(describing: additionalMethods)), ("savedInfo", String(describing: savedInfo)), ("savedCredentials", String(describing: savedCredentials)), ("users", String(describing: users))]) } } @@ -794,17 +799,21 @@ public extension Api.payments { if Int(_1!) & Int(1 << 4) != 0 {if let signature = reader.readInt32() { _11 = Api.parse(reader, signature: signature) as? Api.DataJSON } } - var _12: Api.PaymentRequestedInfo? + var _12: [Api.PaymentFormMethod]? + if Int(_1!) & Int(1 << 6) != 0 {if let _ = reader.readInt32() { + _12 = Api.parseVector(reader, elementSignature: 0, elementType: Api.PaymentFormMethod.self) + } } + var _13: Api.PaymentRequestedInfo? if Int(_1!) & Int(1 << 0) != 0 {if let signature = reader.readInt32() { - _12 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo + _13 = Api.parse(reader, signature: signature) as? Api.PaymentRequestedInfo } } - var _13: Api.PaymentSavedCredentials? + var _14: Api.PaymentSavedCredentials? if Int(_1!) & Int(1 << 1) != 0 {if let signature = reader.readInt32() { - _13 = Api.parse(reader, signature: signature) as? Api.PaymentSavedCredentials + _14 = Api.parse(reader, signature: signature) as? Api.PaymentSavedCredentials } } - var _14: [Api.User]? + var _15: [Api.User]? if let _ = reader.readInt32() { - _14 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) + _15 = Api.parseVector(reader, elementSignature: 0, elementType: Api.User.self) } let _c1 = _1 != nil let _c2 = _2 != nil @@ -817,11 +826,12 @@ public extension Api.payments { let _c9 = _9 != nil let _c10 = (Int(_1!) & Int(1 << 4) == 0) || _10 != nil let _c11 = (Int(_1!) & Int(1 << 4) == 0) || _11 != nil - let _c12 = (Int(_1!) & Int(1 << 0) == 0) || _12 != nil - let _c13 = (Int(_1!) & Int(1 << 1) == 0) || _13 != nil - let _c14 = _14 != nil - if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 { - return Api.payments.PaymentForm.paymentForm(flags: _1!, formId: _2!, botId: _3!, title: _4!, description: _5!, photo: _6, invoice: _7!, providerId: _8!, url: _9!, nativeProvider: _10, nativeParams: _11, savedInfo: _12, savedCredentials: _13, users: _14!) + let _c12 = (Int(_1!) & Int(1 << 6) == 0) || _12 != nil + let _c13 = (Int(_1!) & Int(1 << 0) == 0) || _13 != nil + let _c14 = (Int(_1!) & Int(1 << 1) == 0) || _14 != nil + let _c15 = _15 != nil + if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 { + return Api.payments.PaymentForm.paymentForm(flags: _1!, formId: _2!, botId: _3!, title: _4!, description: _5!, photo: _6, invoice: _7!, providerId: _8!, url: _9!, nativeProvider: _10, nativeParams: _11, additionalMethods: _12, savedInfo: _13, savedCredentials: _14, users: _15!) } else { return nil diff --git a/submodules/TelegramApi/Sources/Api28.swift b/submodules/TelegramApi/Sources/Api28.swift index 1f202e0395..220dbe1deb 100644 --- a/submodules/TelegramApi/Sources/Api28.swift +++ b/submodules/TelegramApi/Sources/Api28.swift @@ -4215,6 +4215,21 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func getFeaturedEmojiStickers(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(248473398) + serializeInt64(hash, buffer: buffer, boxed: false) + return (FunctionDescription(name: "messages.getFeaturedEmojiStickers", parameters: [("hash", String(describing: hash))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.messages.FeaturedStickers? in + let reader = BufferReader(buffer) + var result: Api.messages.FeaturedStickers? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.messages.FeaturedStickers + } + return result + }) + } +} public extension Api.functions.messages { static func getFeaturedStickers(hash: Int64) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() @@ -6295,11 +6310,11 @@ public extension Api.functions.payments { } } public extension Api.functions.payments { - static func canPurchasePremium() -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + static func canPurchasePremium(purpose: Api.InputStorePaymentPurpose) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() - buffer.appendInt32(-1435856696) - - return (FunctionDescription(name: "payments.canPurchasePremium", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + buffer.appendInt32(-1614700874) + purpose.serialize(buffer, true) + return (FunctionDescription(name: "payments.canPurchasePremium", parameters: [("purpose", String(describing: purpose))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in let reader = BufferReader(buffer) var result: Api.Bool? if let signature = reader.readInt32() { diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 6657ba966a..0d65d07d55 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -800,13 +800,13 @@ public final class MediaStreamComponent: CombinedComponent { AnyComponentWithIdentity(id: "a", component: AnyComponent(LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "anim_profilemore", - colors: [ - "Point 2.Group 1.Fill 1": whiteColor, - "Point 3.Group 1.Fill 1": whiteColor, - "Point 1.Group 1.Fill 1": whiteColor - ], mode: .still(position: .begin) ), + colors: [ + "Point 2.Group 1.Fill 1": whiteColor, + "Point 3.Group 1.Fill 1": whiteColor, + "Point 1.Group 1.Fill 1": whiteColor + ], size: CGSize(width: 22.0, height: 22.0) ).tagged(moreAnimationTag))), ])), diff --git a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift index 9c8ebeaf60..d2b70fb1e6 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift @@ -351,6 +351,7 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } var addedHashtags: [String] = [] + var emojiItems: [RecentEmojiItem] = [] var localGroupingKeyBySourceKey: [Int64: Int64] = [:] @@ -475,6 +476,13 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, let hashtag = nsText.substring(with: entityRange) addedHashtags.append(hashtag) } + } else if case let .CustomEmoji(_, fileId) = entity.type { + let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) + if let file = inlineStickers[mediaId] as? TelegramMediaFile { + emojiItems.append(RecentEmojiItem(.file(file))) + } else if let file = transaction.getMedia(mediaId) as? TelegramMediaFile { + emojiItems.append(RecentEmojiItem(.file(file))) + } } } break @@ -786,6 +794,19 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId, } var messageIds: [MessageId?] = [] if !storeMessages.isEmpty { + for emojiItem in emojiItems { + if let entry = CodableEntry(emojiItem) { + let id: RecentEmojiItemId + switch emojiItem.content { + case let .file(file): + id = RecentEmojiItemId(file.fileId) + case let .text(text): + id = RecentEmojiItemId(text) + } + transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: Namespaces.OrderedItemList.LocalRecentEmoji, item: OrderedItemListEntry(id: id.rawValue, contents: entry), removeTailIfCountExceeds: 20) + } + } + let globallyUniqueIdToMessageId = transaction.addMessages(storeMessages, location: .Random) for globallyUniqueId in globallyUniqueIds { messageIds.append(globallyUniqueIdToMessageId[globallyUniqueId]) diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 404bd63899..dfebb69a3d 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -316,6 +316,7 @@ public final class AccountViewTracker { private var channelPollingContexts: [PeerId: ChannelPollingContext] = [:] private var featuredStickerPacksContext: FeaturedStickerPacksContext? + private var featuredEmojiPacksContext: FeaturedStickerPacksContext? let chatHistoryPreloadManager: ChatHistoryPreloadManager @@ -1711,7 +1712,7 @@ public final class AccountViewTracker { let timestamp = CFAbsoluteTimeGetCurrent() if context.timestamp == nil || abs(context.timestamp! - timestamp) > 60.0 * 60.0 { context.timestamp = timestamp - context.disposable.set(updatedFeaturedStickerPacks(network: account.network, postbox: account.postbox).start()) + context.disposable.set(updatedFeaturedStickerPacks(network: account.network, postbox: account.postbox, category: .stickerPacks).start()) } let index = context.subscribers.add(Void()) @@ -1736,6 +1737,56 @@ public final class AccountViewTracker { } } + public func featuredEmojiPacks() -> Signal<[FeaturedStickerPackItem], NoError> { + return Signal { subscriber in + if let account = self.account { + let view = account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)]).start(next: { next in + if let view = next.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)] as? OrderedItemListView { + subscriber.putNext(view.items.map { $0.contents.get(FeaturedStickerPackItem.self)! }) + } else { + subscriber.putNext([]) + } + }, completed: { + subscriber.putCompletion() + }) + let disposable = MetaDisposable() + self.queue.async { + let context: FeaturedStickerPacksContext + if let current = self.featuredEmojiPacksContext { + context = current + } else { + context = FeaturedStickerPacksContext() + self.featuredEmojiPacksContext = context + } + + let timestamp = CFAbsoluteTimeGetCurrent() + if context.timestamp == nil || abs(context.timestamp! - timestamp) > 60.0 * 60.0 { + context.timestamp = timestamp + context.disposable.set(updatedFeaturedStickerPacks(network: account.network, postbox: account.postbox, category: .emojiPacks).start()) + } + + let index = context.subscribers.add(Void()) + + disposable.set(ActionDisposable { + self.queue.async { + if let context = self.featuredEmojiPacksContext { + context.subscribers.remove(index) + } + } + }) + } + return ActionDisposable { + view.dispose() + disposable.dispose() + } + } else { + subscriber.putNext([]) + subscriber.putCompletion() + return EmptyDisposable + } + } + } + public func callListView(type: CallListViewType, index: MessageIndex, count: Int) -> Signal { if let account = self.account { let granularity: Int32 = 60 * 60 * 24 diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift index bd5ca0c408..30ebc429d2 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift @@ -220,10 +220,12 @@ private func installRemoteStickerPacks(network: Network, infos: [StickerPackColl var archivedIds = Set() for archivedSet in archivedSets { switch archivedSet { - case let .stickerSetCovered(set, _): - archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) - case let .stickerSetMultiCovered(set, _): - archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) + case let .stickerSetCovered(set, _): + archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) + case let .stickerSetMultiCovered(set, _): + archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) + case let .stickerSetFullCovered(set, _, _): + archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id) } } return archivedIds diff --git a/submodules/TelegramCore/Sources/State/StickerManagement.swift b/submodules/TelegramCore/Sources/State/StickerManagement.swift index 6ed01b4b11..68ab5b358a 100644 --- a/submodules/TelegramCore/Sources/State/StickerManagement.swift +++ b/submodules/TelegramCore/Sources/State/StickerManagement.swift @@ -3,6 +3,30 @@ import TelegramApi import Postbox import SwiftSignalKit +enum FeaturedStickerPacksCategory { + case stickerPacks + case emojiPacks +} + +extension FeaturedStickerPacksCategory { + var itemListNamespace: Int32 { + switch self { + case .stickerPacks: + return Namespaces.OrderedItemList.CloudFeaturedStickerPacks + case .emojiPacks: + return Namespaces.OrderedItemList.CloudFeaturedEmojiPacks + } + } + + var collectionIdNamespace: Int32 { + switch self { + case .stickerPacks: + return Namespaces.ItemCollection.CloudStickerPacks + case .emojiPacks: + return Namespaces.ItemCollection.CloudEmojiPacks + } + } +} private func hashForIdsReverse(_ ids: [Int64]) -> Int64 { var acc: UInt64 = 0 @@ -24,9 +48,9 @@ func manageStickerPacks(network: Network, postbox: Postbox) -> Signal then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart } -func updatedFeaturedStickerPacks(network: Network, postbox: Postbox) -> Signal { +func updatedFeaturedStickerPacks(network: Network, postbox: Postbox, category: FeaturedStickerPacksCategory) -> Signal { return postbox.transaction { transaction -> Signal in - let initialPacks = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) + let initialPacks = transaction.getOrderedListItems(collectionId: category.itemListNamespace) var initialPackMap: [Int64: FeaturedStickerPackItem] = [:] for entry in initialPacks { let item = entry.contents.get(FeaturedStickerPackItem.self)! @@ -37,18 +61,29 @@ func updatedFeaturedStickerPacks(network: Network, postbox: Postbox) -> Signal retryRequest - |> mapToSignal { result -> Signal in - return postbox.transaction { transaction -> Void in + + struct FeaturedListContent { + var unreadIds: Set + var packs: [FeaturedStickerPackItem] + var isPremium: Bool + } + enum FeaturedList { + case notModified + case content(FeaturedListContent) + } + let signal: Signal + switch category { + case .stickerPacks: + signal = network.request(Api.functions.messages.getFeaturedStickers(hash: initialHash)) + |> map { result -> FeaturedList in switch result { case .featuredStickersNotModified: - break + return .notModified case let .featuredStickers(flags, _, _, sets, unread): let unreadIds = Set(unread) var updatedPacks: [FeaturedStickerPackItem] = [] for set in sets { - var (info, items) = parsePreviewStickerSet(set) + var (info, items) = parsePreviewStickerSet(set, namespace: category.collectionIdNamespace) if let previousPack = initialPackMap[info.id.id] { if previousPack.info.hash == info.hash { items = previousPack.topItems @@ -56,7 +91,56 @@ func updatedFeaturedStickerPacks(network: Network, postbox: Postbox) -> Signal OrderedItemListEntry? in + let isPremium = flags & (1 << 0) != 0 + return .content(FeaturedListContent( + unreadIds: unreadIds, + packs: updatedPacks, + isPremium: isPremium + )) + } + } + |> `catch` { _ -> Signal in + return .single(.notModified) + } + case .emojiPacks: + signal = network.request(Api.functions.messages.getFeaturedEmojiStickers(hash: initialHash)) + |> map { result -> FeaturedList in + switch result { + case .featuredStickersNotModified: + return .notModified + case let .featuredStickers(flags, _, _, sets, unread): + let unreadIds = Set(unread) + var updatedPacks: [FeaturedStickerPackItem] = [] + for set in sets { + var (info, items) = parsePreviewStickerSet(set, namespace: category.collectionIdNamespace) + if let previousPack = initialPackMap[info.id.id] { + if previousPack.info.hash == info.hash { + items = previousPack.topItems + } + } + updatedPacks.append(FeaturedStickerPackItem(info: info, topItems: items, unread: unreadIds.contains(info.id.id))) + } + let isPremium = flags & (1 << 0) != 0 + return .content(FeaturedListContent( + unreadIds: unreadIds, + packs: updatedPacks, + isPremium: isPremium + )) + } + } + |> `catch` { _ -> Signal in + return .single(.notModified) + } + } + + return signal + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Void in + switch result { + case .notModified: + break + case let .content(content): + transaction.replaceOrderedItemListItems(collectionId: category.itemListNamespace, items: content.packs.compactMap { item -> OrderedItemListEntry? in if let entry = CodableEntry(item) { return OrderedItemListEntry(id: FeaturedStickerPackItemId(item.info.id.id).rawValue, contents: entry) } else { @@ -64,14 +148,14 @@ func updatedFeaturedStickerPacks(network: Network, postbox: Postbox) -> Signal switchToLatest + } + |> switchToLatest } public func requestOldFeaturedStickerPacks(network: Network, postbox: Postbox, offset: Int, limit: Int) -> Signal<[FeaturedStickerPackItem], NoError> { @@ -85,7 +169,7 @@ public func requestOldFeaturedStickerPacks(network: Network, postbox: Postbox, o let unreadIds = Set(unread) var updatedPacks: [FeaturedStickerPackItem] = [] for set in sets { - let (info, items) = parsePreviewStickerSet(set) + let (info, items) = parsePreviewStickerSet(set, namespace: Namespaces.ItemCollection.CloudStickerPacks) updatedPacks.append(FeaturedStickerPackItem(info: info, topItems: items, unread: unreadIds.contains(info.id.id))) } return updatedPacks @@ -125,23 +209,55 @@ public func preloadedFeaturedStickerSet(network: Network, postbox: Postbox, id: } |> switchToLatest } -func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollectionId.Namespace = Namespaces.ItemCollection.CloudStickerPacks) -> (StickerPackCollectionInfo, [StickerPackItem]) { +func parsePreviewStickerSet(_ set: Api.StickerSetCovered, namespace: ItemCollectionId.Namespace) -> (StickerPackCollectionInfo, [StickerPackItem]) { switch set { - case let .stickerSetCovered(set, cover): - let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) - var items: [StickerPackItem] = [] + case let .stickerSetCovered(set, cover): + let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) + var items: [StickerPackItem] = [] + if let file = telegramMediaFileFromApiDocument(cover), let id = file.id { + items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: [])) + } + return (info, items) + case let .stickerSetMultiCovered(set, covers): + let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) + var items: [StickerPackItem] = [] + for cover in covers { if let file = telegramMediaFileFromApiDocument(cover), let id = file.id { items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: [])) } - return (info, items) - case let .stickerSetMultiCovered(set, covers): - let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) - var items: [StickerPackItem] = [] - for cover in covers { - if let file = telegramMediaFileFromApiDocument(cover), let id = file.id { - items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: [])) + } + return (info, items) + case let .stickerSetFullCovered(set, packs, documents): + var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:] + for pack in packs { + switch pack { + case let .stickerPack(text, fileIds): + let key = ValueBoxKey(text).toMemoryBuffer() + for fileId in fileIds { + let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId) + if indexKeysByFile[mediaId] == nil { + indexKeysByFile[mediaId] = [key] + } else { + indexKeysByFile[mediaId]!.append(key) + } } + break } - return (info, items) + } + + let info = StickerPackCollectionInfo(apiSet: set, namespace: namespace) + var items: [StickerPackItem] = [] + for document in documents { + if let file = telegramMediaFileFromApiDocument(document), let id = file.id { + let fileIndexKeys: [MemoryBuffer] + if let indexKeys = indexKeysByFile[id] { + fileIndexKeys = indexKeys + } else { + fileIndexKeys = [] + } + items.append(StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: id.id), file: file, indexKeys: fileIndexKeys)) + } + } + return (info, items) } } diff --git a/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift b/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift index 4078d75f68..714ebe6242 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeRecentlyUsedMediaOperations.swift @@ -45,3 +45,6 @@ func _internal_clearRecentlyUsedStickers(transaction: Transaction) { addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .clear) } +func _internal_clearRecentlyUsedEmoji(transaction: Transaction) { + transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.LocalRecentEmoji, items: []) +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index e50e1cefde..21b6d87f87 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -64,6 +64,8 @@ public struct Namespaces { public static let RecentDownloads: Int32 = 11 public static let PremiumStickers: Int32 = 12 public static let CloudPremiumStickers: Int32 = 13 + public static let LocalRecentEmoji: Int32 = 14 + public static let CloudFeaturedEmojiPacks: Int32 = 15 } public struct CachedItemCollection { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift index 74999632b9..c62293053d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_RecentMediaItem.swift @@ -49,3 +49,106 @@ public final class RecentMediaItem: Codable, Equatable { return lhs.media.isEqual(to: rhs.media) } } + +public struct RecentEmojiItemId { + public enum Id { + case media(MediaId) + case text(String) + } + + public let rawValue: MemoryBuffer + public let id: Id + + public init(_ rawValue: MemoryBuffer) { + self.rawValue = rawValue + + assert(rawValue.length >= 1) + var type: UInt8 = 0 + memcpy(&type, rawValue.memory.advanced(by: 0), 1) + + if type == 0 { + assert(rawValue.length == 1 + 4 + 8) + var mediaIdNamespace: Int32 = 0 + var mediaIdId: Int64 = 0 + memcpy(&mediaIdNamespace, rawValue.memory.advanced(by: 1), 4) + memcpy(&mediaIdId, rawValue.memory.advanced(by: 1 + 4), 8) + self.id = .media(MediaId(namespace: mediaIdNamespace, id: mediaIdId)) + } else if type == 1 { + var length: UInt16 = 0 + assert(rawValue.length >= 1 + 2) + memcpy(&length, rawValue.memory.advanced(by: 1), 2) + + assert(rawValue.length >= 1 + 2 + Int(length)) + + self.id = .text(String(data: Data(bytes: rawValue.memory.advanced(by: 1 + 2), count: Int(length)), encoding: .utf8) ?? ".") + } else { + assert(false) + self.id = .text(".") + } + } + + public init(_ mediaId: MediaId) { + self.id = .media(mediaId) + + var mediaIdNamespace: Int32 = mediaId.namespace + var mediaIdId: Int64 = mediaId.id + self.rawValue = MemoryBuffer(memory: malloc(1 + 4 + 8)!, capacity: 1 + 4 + 8, length: 1 + 4 + 8, freeWhenDone: true) + var type: UInt8 = 0 + memcpy(self.rawValue.memory.advanced(by: 0), &type, 1) + memcpy(self.rawValue.memory.advanced(by: 1), &mediaIdNamespace, 4) + memcpy(self.rawValue.memory.advanced(by: 1 + 4), &mediaIdId, 8) + } + + public init(_ text: String) { + self.id = .text(text) + + let data = text.data(using: .utf8) ?? Data() + var length: UInt16 = UInt16(data.count) + + self.rawValue = MemoryBuffer(memory: malloc(1 + 2 + data.count)!, capacity: 1 + 2 + data.count, length: 1 + 2 + data.count, freeWhenDone: true) + var type: UInt8 = 1 + memcpy(self.rawValue.memory.advanced(by: 0), &type, 1) + memcpy(self.rawValue.memory.advanced(by: 1), &length, 2) + data.withUnsafeBytes { bytes in + let _ = memcpy(self.rawValue.memory.advanced(by: 1 + 2), bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), bytes.count) + } + } +} + +public final class RecentEmojiItem: Codable, Equatable { + public enum Content: Equatable { + case file(TelegramMediaFile) + case text(String) + } + + public let content: Content + + public init(_ content: Content) { + self.content = content + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + if let mediaData = try container.decodeIfPresent(AdaptedPostboxDecoder.RawObjectData.self, forKey: "m") { + self.content = .file(TelegramMediaFile(decoder: PostboxDecoder(buffer: MemoryBuffer(data: mediaData.data)))) + } else { + self.content = .text(try container.decode(String.self, forKey: "s")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + switch self.content { + case let .file(file): + try container.encode(PostboxEncoder().encodeObjectToRawData(file), forKey: "m") + case let .text(string): + try container.encode(string, forKey: "s") + } + } + + public static func ==(lhs: RecentEmojiItem, rhs: RecentEmojiItem) -> Bool { + return lhs.content == rhs.content + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift index 02d4e6ac9c..125cf90fad 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/AppStore.swift @@ -59,7 +59,7 @@ public enum RestoreAppStoreReceiptError { } func _internal_canPurchasePremium(account: Account) -> Signal { - return account.network.request(Api.functions.payments.canPurchasePremium()) + return account.network.request(Api.functions.payments.canPurchasePremium(purpose: .inputStorePaymentPremiumSubscription(flags: 0))) |> map { result -> Bool in switch result { case .boolTrue: diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift index 4518080812..547c67571f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Payments/BotPaymentForm.swift @@ -213,7 +213,7 @@ func _internal_fetchBotPaymentInvoice(postbox: Postbox, network: Network, source |> mapToSignal { result -> Signal in return postbox.transaction { transaction -> TelegramMediaInvoice in switch result { - case let .paymentForm(_, _, _, title, description, photo, invoice, _, _, _, _, _, _, _): + case let .paymentForm(_, _, _, title, description, photo, invoice, _, _, _, _, _, _, _, _): let parsedInvoice = BotPaymentInvoice(apiInvoice: invoice) var parsedFlags = TelegramMediaInvoiceFlags() @@ -266,10 +266,11 @@ func _internal_fetchBotPaymentForm(postbox: Postbox, network: Network, source: B |> mapToSignal { result -> Signal in return postbox.transaction { transaction -> BotPaymentForm in switch result { - case let .paymentForm(flags, id, botId, title, description, photo, invoice, providerId, url, nativeProvider, nativeParams, savedInfo, savedCredentials, apiUsers): + case let .paymentForm(flags, id, botId, title, description, photo, invoice, providerId, url, nativeProvider, nativeParams, additionalMethods, savedInfo, savedCredentials, apiUsers): let _ = title let _ = description let _ = photo + let _ = additionalMethods var peers: [Peer] = [] for user in apiUsers { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift index 7d61a5a8f3..954e7ef206 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/SearchStickers.swift @@ -306,7 +306,7 @@ func _internal_searchStickerSetsRemotely(network: Network, query: String) -> Sig case let .foundStickerSets(_, sets: sets): var result = FoundStickerSets() for set in sets { - let parsed = parsePreviewStickerSet(set) + let parsed = parsePreviewStickerSet(set, namespace: Namespaces.ItemCollection.CloudStickerPacks) let values = parsed.1.map({ ItemCollectionViewEntry(index: ItemCollectionViewEntryIndex(collectionIndex: index, collectionId: parsed.0.id, itemIndex: $0.index), item: $0) }) result = result.withUpdatedInfosAndEntries(infos: [(parsed.0.id, parsed.0, parsed.1.first, false)], entries: values) index += 1 diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift index ec51d20bd3..4fb16e89f7 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift @@ -105,7 +105,7 @@ func _internal_stickerPacksAttachedToMedia(account: Account, media: AnyMediaRefe |> map { result -> [StickerPackReference] in return result.map { pack in switch pack { - case let .stickerSetCovered(set, _), let .stickerSetMultiCovered(set, _): + case let .stickerSetCovered(set, _), let .stickerSetMultiCovered(set, _), let .stickerSetFullCovered(set, _, _): let info = StickerPackCollectionInfo(apiSet: set, namespace: Namespaces.ItemCollection.CloudStickerPacks) return .id(id: info.id.id, accessHash: info.accessHash) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift index 57f9fbc734..a09adceb7e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerSetInstallation.swift @@ -140,64 +140,69 @@ public final class CoveredStickerSet : Equatable { func _internal_installStickerSetInteractively(account: Account, info: StickerPackCollectionInfo, items: [ItemCollectionItem]) -> Signal { return account.network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), archived: .boolFalse)) |> mapError { _ -> InstallStickerSetError in return .generic - } |> mapToSignal { result -> Signal in - let addResult:InstallStickerSetResult - switch result { - case .stickerSetInstallResultSuccess: - addResult = .successful - case let .stickerSetInstallResultArchive(sets: archived): - var coveredSets:[CoveredStickerSet] = [] - for archived in archived { - let apiDocuments:[Api.Document] - let apiSet:Api.StickerSet - switch archived { - case let .stickerSetCovered(set: set, cover: cover): - apiSet = set - apiDocuments = [cover] - case let .stickerSetMultiCovered(set: set, covers: covers): - apiSet = set - apiDocuments = covers + } + |> mapToSignal { result -> Signal in + let addResult:InstallStickerSetResult + switch result { + case .stickerSetInstallResultSuccess: + addResult = .successful + case let .stickerSetInstallResultArchive(sets: archived): + var coveredSets:[CoveredStickerSet] = [] + for archived in archived { + let apiDocuments:[Api.Document] + let apiSet:Api.StickerSet + switch archived { + case let .stickerSetCovered(set: set, cover: cover): + apiSet = set + apiDocuments = [cover] + case let .stickerSetMultiCovered(set: set, covers: covers): + apiSet = set + apiDocuments = covers + case let .stickerSetFullCovered(set, _, documents): + apiSet = set + apiDocuments = documents + } + + let info = StickerPackCollectionInfo(apiSet: apiSet, namespace: Namespaces.ItemCollection.CloudStickerPacks) + + var items:[StickerPackItem] = [] + for apiDocument in apiDocuments { + if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { + items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: [])) } - - let info = StickerPackCollectionInfo(apiSet: apiSet, namespace: Namespaces.ItemCollection.CloudStickerPacks) - - var items:[StickerPackItem] = [] - for apiDocument in apiDocuments { - if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id { - items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: [])) + } + coveredSets.append(CoveredStickerSet(info: info, items: items)) + } + addResult = .archived(coveredSets) + } + + + return account.postbox.transaction { transaction -> Void in + var collections = transaction.getCollectionsItems(namespace: info.id.namespace) + + var removableIndexes:[Int] = [] + for i in 0 ..< collections.count { + if collections[i].0 == info.id { + removableIndexes.append(i) + } + if case let .archived(sets) = addResult { + for set in sets { + if collections[i].0 == set.info.id { + removableIndexes.append(i) } } - coveredSets.append(CoveredStickerSet(info: info, items: items)) } - addResult = .archived(coveredSets) } + for index in removableIndexes.reversed() { + collections.remove(at: index) + } - return account.postbox.transaction { transaction -> Void in - var collections = transaction.getCollectionsItems(namespace: info.id.namespace) - - var removableIndexes:[Int] = [] - for i in 0 ..< collections.count { - if collections[i].0 == info.id { - removableIndexes.append(i) - } - if case let .archived(sets) = addResult { - for set in sets { - if collections[i].0 == set.info.id { - removableIndexes.append(i) - } - } - } - } - - for index in removableIndexes.reversed() { - collections.remove(at: index) - } - - collections.insert((info.id, info, items), at: 0) - - transaction.replaceItemCollections(namespace: info.id.namespace, itemCollections: collections) - } |> map { _ in return addResult} |> mapError { _ -> InstallStickerSetError in } + collections.insert((info.id, info, items), at: 0) + + transaction.replaceItemCollections(namespace: info.id.namespace, itemCollections: collections) + } + |> map { _ in return addResult} |> mapError { _ -> InstallStickerSetError in } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift index 6572932b23..936b704f76 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/TelegramEngineStickers.swift @@ -133,6 +133,13 @@ public extension TelegramEngine { |> ignoreValues } + public func clearRecentlyUsedEmoji() -> Signal { + return self.account.postbox.transaction { transaction -> Void in + _internal_clearRecentlyUsedEmoji(transaction: transaction) + } + |> ignoreValues + } + public func reorderStickerPacks(namespace: ItemCollectionId.Namespace, itemIds: [ItemCollectionId]) -> Signal { return self.account.postbox.transaction { transaction -> Void in let infos = transaction.getItemCollectionsInfos(namespace: namespace) diff --git a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift index 1257695a51..331c154d89 100644 --- a/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift +++ b/submodules/TelegramPresentationData/Sources/DefaultDarkPresentationTheme.swift @@ -529,7 +529,7 @@ public func makeDefaultDarkPresentationTheme(extendingThemeReference: Presentati reactionActiveBackground: UIColor(rgb: 0xffffff, alpha: 1.0), reactionActiveForeground: .clear ) - ), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0x313131).mixedWith(UIColor(rgb: 0xffffff), alpha: 0.05), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff) + ), primaryTextColor: UIColor(rgb: 0xffffff), secondaryTextColor: UIColor(rgb: 0xffffff, alpha: 0.5), linkTextColor: UIColor(rgb: 0xffffff), linkHighlightColor: UIColor(rgb: 0xffffff, alpha: 0.5), scamColor: UIColor(rgb: 0xeb5545), textHighlightColor: UIColor(rgb: 0xf5c038), accentTextColor: UIColor(rgb: 0xffffff), accentControlColor: UIColor(rgb: 0xffffff), accentControlDisabledColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaActiveControlColor: UIColor(rgb: 0xffffff), mediaInactiveControlColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaControlInnerBackgroundColor: UIColor(rgb: 0x313131), pendingActivityColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileTitleColor: UIColor(rgb: 0xffffff), fileDescriptionColor: UIColor(rgb: 0xffffff, alpha: 0.5), fileDurationColor: UIColor(rgb: 0xffffff, alpha: 0.5), mediaPlaceholderColor: UIColor(rgb: 0xffffff, alpha: 0.2), polls: PresentationThemeChatBubblePolls(radioButton: UIColor(rgb: 0xffffff), radioProgress: UIColor(rgb: 0xffffff), highlight: UIColor(rgb: 0xffffff).withAlphaComponent(0.12), separator: UIColor(rgb: 0xffffff, alpha: 0.5), bar: UIColor(rgb: 0xffffff), barIconForeground: .clear, barPositive: UIColor(rgb: 0xffffff), barNegative: UIColor(rgb: 0xffffff)), actionButtonsFillColor: PresentationThemeVariableColor(withWallpaper: UIColor(rgb: 0x000000, alpha: 0.5), withoutWallpaper: UIColor(rgb: 0x000000, alpha: 0.5)), actionButtonsStrokeColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xb2b2b2, alpha: 0.18)), actionButtonsTextColor: PresentationThemeVariableColor(color: UIColor(rgb: 0xffffff)), textSelectionColor: UIColor(rgb: 0xffffff, alpha: 0.2), textSelectionKnobColor: UIColor(rgb: 0xffffff) ), freeform: PresentationThemeBubbleColor( withWallpaper: PresentationThemeBubbleColorComponents( diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index e7e93439fb..280bae0544 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -373,7 +373,7 @@ public struct PresentationResourcesChat { public static func chatInputMediaPanelGridDismissImage(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatInputMediaPanelGridDismissImage.rawValue, { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/GridDismissIcon"), color: theme.chat.inputMediaPanel.panelIconColor.withAlphaComponent(0.65)) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/GridDismissIcon"), color: theme.chat.inputMediaPanel.stickersSectionTextColor) }) } diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 09c9af7a1c..5e6e58c568 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -47,29 +47,21 @@ public final class AnimationCacheItemFrame { } public final class AnimationCacheItem { + public enum Advance { + case duration(Double) + case frames(Int) + } + public let numFrames: Int - private let getFrameImpl: (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? - private let getFrameIndexImpl: (Double) -> Int - private let getFrameDurationImpl: (Int) -> Double? + private let advanceImpl: (Advance, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? - public init(numFrames: Int, getFrame: @escaping (Int, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?, getFrameIndexImpl: @escaping (Double) -> Int, getFrameDurationImpl: @escaping (Int) -> Double?) { + public init(numFrames: Int, advanceImpl: @escaping (Advance, AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame?) { self.numFrames = numFrames - self.getFrameImpl = getFrame - self.getFrameIndexImpl = getFrameIndexImpl - self.getFrameDurationImpl = getFrameDurationImpl + self.advanceImpl = advanceImpl } - public func getFrameDuration(index: Int) -> Double? { - return self.getFrameDurationImpl(index) - } - - public func getFrame(index: Int, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? { - return self.getFrameImpl(index, requestedFormat) - } - - public func getFrame(at duration: Double, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? { - let index = self.getFrameIndexImpl(duration) - return self.getFrameImpl(index, requestedFormat) + public func advance(advance: Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? { + return self.advanceImpl(advance, requestedFormat) } } @@ -99,7 +91,7 @@ public protocol AnimationCacheItemWriter: AnyObject { var queue: Queue { get } var isCancelled: Bool { get } - func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Void, proposedWidth: Int, proposedHeight: Int, duration: Double) + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int) func finish() } @@ -216,21 +208,23 @@ private func decompressData(data: Data, range: Range, decompressedSize: Int } private final class AnimationCacheItemWriterInternal { + enum WriteError: Error { + case generic + } + struct CompressedResult { var path: String } private struct FrameMetadata { - var offset: Int - var length: Int var duration: Double } var isCancelled: Bool = false - private let decompressedPath: String private let compressedPath: String - private var file: ManagedFile? + private let file: ManagedFile + private let compressedWriter: CompressedFileWriter private var currentYUVASurface: ImageYUVA420? private var currentDctData: DctData? @@ -240,28 +234,30 @@ private final class AnimationCacheItemWriterInternal { private var isFinished: Bool = false private var frames: [FrameMetadata] = [] - private var contentLength: Int = 0 private let dctQuality: Int init?(allocateTempFile: @escaping () -> String) { self.dctQuality = 70 - self.decompressedPath = allocateTempFile() self.compressedPath = allocateTempFile() - guard let file = ManagedFile(queue: nil, path: self.decompressedPath, mode: .readwrite) else { + guard let file = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { + return nil + } + guard let compressedWriter = CompressedFileWriter(file: file) else { return nil } self.file = file + self.compressedWriter = compressedWriter } - func add(with drawingBlock: (ImageYUVA420) -> Void, proposedWidth: Int, proposedHeight: Int, duration: Double) { + func add(with drawingBlock: (ImageYUVA420) -> Double?, proposedWidth: Int, proposedHeight: Int) throws { if self.isFailed || self.isFinished { return } - guard !self.isFailed, !self.isFinished, let file = self.file else { + guard !self.isFailed, !self.isFinished else { return } @@ -305,24 +301,27 @@ private final class AnimationCacheItemWriterInternal { self.currentDctData = dctData } - drawingBlock(yuvaSurface) + let duration = drawingBlock(yuvaSurface) + + guard let duration = duration else { + return + } yuvaSurface.dct(dctData: dctData, target: dctCoefficients) if isFirstFrame { - file.write(2 as UInt32) + self.file.write(3 as UInt32) - file.write(UInt32(dctCoefficients.yPlane.width)) - file.write(UInt32(dctCoefficients.yPlane.height)) - file.write(UInt32(dctData.quality)) + self.file.write(UInt32(dctCoefficients.yPlane.width)) + self.file.write(UInt32(dctCoefficients.yPlane.height)) + self.file.write(UInt32(dctData.quality)) - self.contentLengthOffset = Int(file.position()) - file.write(0 as UInt32) + self.contentLengthOffset = Int(self.file.position()) + self.file.write(0 as UInt32) } - let framePosition = Int(file.position()) - assert(framePosition >= 0) - var frameLength = 0 + let frameLength = dctCoefficients.yPlane.data.count + dctCoefficients.uPlane.data.count + dctCoefficients.vPlane.data.count + dctCoefficients.aPlane.data.count + try self.compressedWriter.writeUInt32(UInt32(frameLength)) for i in 0 ..< 4 { let dctPlane: DctCoefficientPlane @@ -339,85 +338,63 @@ private final class AnimationCacheItemWriterInternal { preconditionFailure() } - dctPlane.data.withUnsafeBytes { bytes in - let _ = file.write(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) + try self.compressedWriter.writeUInt32(UInt32(dctPlane.data.count)) + try dctPlane.data.withUnsafeBytes { bytes in + try self.compressedWriter.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) } - frameLength += dctPlane.data.count } - self.frames.append(FrameMetadata(offset: framePosition, length: frameLength, duration: duration)) - - self.contentLength += frameLength + self.frames.append(FrameMetadata(duration: duration)) } - func finish() -> CompressedResult? { + func finish() throws -> CompressedResult { var shouldComplete = false - outer: for _ in 0 ..< 1 { + do { if !self.isFinished { self.isFinished = true shouldComplete = true - guard let contentLengthOffset = self.contentLengthOffset, let file = self.file else { + try self.compressedWriter.flush() + + guard let contentLengthOffset = self.contentLengthOffset else { self.isFailed = true - break outer + throw WriteError.generic } assert(contentLengthOffset >= 0) let metadataPosition = file.position() + let contentLength = Int(metadataPosition) - contentLengthOffset - 4 file.seek(position: Int64(contentLengthOffset)) - file.write(UInt32(self.contentLength)) + file.write(UInt32(contentLength)) file.seek(position: metadataPosition) file.write(UInt32(self.frames.count)) for frame in self.frames { - file.write(UInt32(frame.offset)) - file.write(UInt32(frame.length)) file.write(Float32(frame.duration)) } if !self.frames.isEmpty { } else { self.isFailed = true - break outer + throw WriteError.generic } - if !self.isFailed { - self.file = nil - - file._unsafeClose() - - guard let uncompressedData = try? Data(contentsOf: URL(fileURLWithPath: self.decompressedPath), options: .alwaysMapped) else { - self.isFailed = true - break outer - } - guard let compressedData = compressData(data: uncompressedData) else { - self.isFailed = true - break outer - } - guard let compressedFile = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { - self.isFailed = true - break outer - } - compressedFile.write(Int32(uncompressedData.count)) - let _ = compressedFile.write(compressedData) - compressedFile._unsafeClose() - } + self.file._unsafeClose() } + } catch let e { + throw e } if shouldComplete { - let _ = try? FileManager.default.removeItem(atPath: self.decompressedPath) - if !self.isFailed { return CompressedResult(path: self.compressedPath) } else { let _ = try? FileManager.default.removeItem(atPath: self.compressedPath) - - return nil + throw WriteError.generic } } else { - return nil + throw WriteError.generic } } } @@ -425,22 +402,18 @@ private final class AnimationCacheItemWriterInternal { private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { struct CompressedResult { var animationPath: String - var firstFramePath: String } private struct FrameMetadata { - var offset: Int - var length: Int var duration: Double } let queue: Queue var isCancelled: Bool = false - private let decompressedPath: String private let compressedPath: String - private let firstFramePath: String private var file: ManagedFile? + private var compressedWriter: CompressedFileWriter? private let completion: (CompressedResult?) -> Void private var currentSurface: ImageARGB? @@ -452,7 +425,6 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { private var isFinished: Bool = false private var frames: [FrameMetadata] = [] - private var contentLength: Int = 0 private let dctQuality: Int @@ -462,24 +434,23 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { self.dctQuality = 70 self.queue = queue - self.decompressedPath = allocateTempFile() self.compressedPath = allocateTempFile() - self.firstFramePath = allocateTempFile() - guard let file = ManagedFile(queue: nil, path: self.decompressedPath, mode: .readwrite) else { + guard let file = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { return nil } self.file = file + self.compressedWriter = CompressedFileWriter(file: file) self.completion = completion } - func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Void, proposedWidth: Int, proposedHeight: Int, duration: Double) { + func add(with drawingBlock: (AnimationCacheItemDrawingSurface) -> Double?, proposedWidth: Int, proposedHeight: Int) { if self.isFailed || self.isFinished { return } self.lock.locked { - guard !self.isFailed, !self.isFinished, let file = self.file else { + guard !self.isFailed, !self.isFinished, let file = self.file, let compressedWriter = self.compressedWriter else { return } @@ -537,8 +508,8 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { self.currentDctData = dctData } - surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Void in - drawingBlock(AnimationCacheItemDrawingSurface( + let duration = surface.argbPlane.data.withUnsafeMutableBytes { bytes -> Double? in + return drawingBlock(AnimationCacheItemDrawingSurface( argb: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), width: width, height: height, @@ -547,11 +518,15 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { )) } + guard let duration = duration else { + return + } + surface.toYUVA420(target: yuvaSurface) yuvaSurface.dct(dctData: dctData, target: dctCoefficients) if isFirstFrame { - file.write(2 as UInt32) + file.write(3 as UInt32) file.write(UInt32(dctCoefficients.yPlane.width)) file.write(UInt32(dctCoefficients.yPlane.height)) @@ -561,34 +536,35 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { file.write(0 as UInt32) } - let framePosition = Int(file.position()) - assert(framePosition >= 0) - var frameLength = 0 - - for i in 0 ..< 4 { - let dctPlane: DctCoefficientPlane - switch i { - case 0: - dctPlane = dctCoefficients.yPlane - case 1: - dctPlane = dctCoefficients.uPlane - case 2: - dctPlane = dctCoefficients.vPlane - case 3: - dctPlane = dctCoefficients.aPlane - default: - preconditionFailure() + do { + let frameLength = dctCoefficients.yPlane.data.count + dctCoefficients.uPlane.data.count + dctCoefficients.vPlane.data.count + dctCoefficients.aPlane.data.count + try compressedWriter.writeUInt32(UInt32(frameLength)) + + for i in 0 ..< 4 { + let dctPlane: DctCoefficientPlane + switch i { + case 0: + dctPlane = dctCoefficients.yPlane + case 1: + dctPlane = dctCoefficients.uPlane + case 2: + dctPlane = dctCoefficients.vPlane + case 3: + dctPlane = dctCoefficients.aPlane + default: + preconditionFailure() + } + + try compressedWriter.writeUInt32(UInt32(dctPlane.data.count)) + try dctPlane.data.withUnsafeBytes { bytes in + try compressedWriter.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) + } } - dctPlane.data.withUnsafeBytes { bytes in - let _ = file.write(bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) - } - frameLength += dctPlane.data.count + self.frames.append(FrameMetadata(duration: duration)) + } catch { + self.isFailed = true } - - self.frames.append(FrameMetadata(offset: framePosition, length: frameLength, duration: duration)) - - self.contentLength += frameLength } } @@ -599,94 +575,43 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { self.isFinished = true shouldComplete = true - guard let contentLengthOffset = self.contentLengthOffset, let file = self.file else { + guard let contentLengthOffset = self.contentLengthOffset, let file = self.file, let compressedWriter = self.compressedWriter else { self.isFailed = true return } assert(contentLengthOffset >= 0) - let metadataPosition = file.position() - file.seek(position: Int64(contentLengthOffset)) - file.write(UInt32(self.contentLength)) + do { + try compressedWriter.flush() - file.seek(position: metadataPosition) - file.write(UInt32(self.frames.count)) - for frame in self.frames { - file.write(UInt32(frame.offset)) - file.write(UInt32(frame.length)) - file.write(Float32(frame.duration)) - } - - if !self.frames.isEmpty, let dctCoefficients = self.currentDctCoefficients, let dctData = self.currentDctData { - var firstFrameData = Data(capacity: 4 * 5 + self.frames[0].length) + let metadataPosition = file.position() + let contentLength = Int(metadataPosition) - contentLengthOffset - 4 + file.seek(position: Int64(contentLengthOffset)) + file.write(UInt32(contentLength)) - writeUInt32(data: &firstFrameData, value: 2 as UInt32) - writeUInt32(data: &firstFrameData, value: UInt32(dctCoefficients.yPlane.width)) - writeUInt32(data: &firstFrameData, value: UInt32(dctCoefficients.yPlane.height)) - writeUInt32(data: &firstFrameData, value: UInt32(dctData.quality)) - - writeUInt32(data: &firstFrameData, value: UInt32(self.frames[0].length)) - let firstFrameStart = 4 * 5 - - file.seek(position: Int64(self.frames[0].offset)) - firstFrameData.count += self.frames[0].length - firstFrameData.withUnsafeMutableBytes { bytes in - let _ = file.read(bytes.baseAddress!.advanced(by: 4 * 5), self.frames[0].length) + file.seek(position: metadataPosition) + file.write(UInt32(self.frames.count)) + for frame in self.frames { + file.write(Float32(frame.duration)) } - writeUInt32(data: &firstFrameData, value: UInt32(1)) - writeUInt32(data: &firstFrameData, value: UInt32(firstFrameStart)) - writeUInt32(data: &firstFrameData, value: UInt32(self.frames[0].length)) - writeFloat32(data: &firstFrameData, value: Float32(1.0)) - - guard let compressedFirstFrameData = compressData(data: firstFrameData, addSizeHeader: true) else { - self.isFailed = true - return + if !self.isFailed { + self.compressedWriter = nil + self.file = nil + + file._unsafeClose() } - guard let _ = try? compressedFirstFrameData.write(to: URL(fileURLWithPath: self.firstFramePath)) else { - self.isFailed = true - return - } - } else { + } catch { self.isFailed = true - return - } - - if !self.isFailed { - self.file = nil - - file._unsafeClose() - - guard let uncompressedData = try? Data(contentsOf: URL(fileURLWithPath: self.decompressedPath), options: .alwaysMapped) else { - self.isFailed = true - return - } - guard let compressedData = compressData(data: uncompressedData) else { - self.isFailed = true - return - } - guard let compressedFile = ManagedFile(queue: nil, path: self.compressedPath, mode: .readwrite) else { - self.isFailed = true - return - } - compressedFile.write(Int32(uncompressedData.count)) - let _ = compressedFile.write(compressedData) - compressedFile._unsafeClose() } } } if shouldComplete { - let _ = try? FileManager.default.removeItem(atPath: self.decompressedPath) - if !self.isFailed { - self.completion(CompressedResult( - animationPath: self.compressedPath, - firstFramePath: self.firstFramePath - )) + self.completion(CompressedResult(animationPath: self.compressedPath)) } else { let _ = try? FileManager.default.removeItem(atPath: self.compressedPath) - let _ = try? FileManager.default.removeItem(atPath: self.firstFramePath) self.completion(nil) } } @@ -694,73 +619,165 @@ private final class AnimationCacheItemWriterImpl: AnimationCacheItemWriter { } private final class AnimationCacheItemAccessor { + private enum ReadError: Error { + case generic + } + + final class CurrentFrame { + let index: Int + var remainingDuration: Double + let duration: Double + let dctCoefficients: DctCoefficientsYUVA420 + + init(index: Int, duration: Double, dctCoefficients: DctCoefficientsYUVA420) { + self.index = index + self.duration = duration + self.remainingDuration = duration + self.dctCoefficients = dctCoefficients + } + } + struct FrameInfo { - let range: Range let duration: Double } private let data: Data + private var compressedDataReader: DecompressedData? + private let range: Range private let frameMapping: [Int: FrameInfo] + private let width: Int + private let height: Int private let durationMapping: [Double] - private let totalDuration: Double + + private var currentFrame: CurrentFrame? private var currentYUVASurface: ImageYUVA420? private var currentDctData: DctData - private var currentDctCoefficients: DctCoefficientsYUVA420 + private var sharedDctCoefficients: DctCoefficientsYUVA420? - init(data: Data, frameMapping: [FrameInfo], width: Int, height: Int, dctQuality: Int) { + init(data: Data, range: Range, frameMapping: [FrameInfo], width: Int, height: Int, dctQuality: Int) { self.data = data + self.range = range + self.width = width + self.height = height var resultFrameMapping: [Int: FrameInfo] = [:] var durationMapping: [Double] = [] - var totalDuration: Double = 0.0 for i in 0 ..< frameMapping.count { let frame = frameMapping[i] resultFrameMapping[i] = frame - totalDuration += frame.duration - durationMapping.append(totalDuration) + durationMapping.append(frame.duration) } self.frameMapping = resultFrameMapping self.durationMapping = durationMapping - self.totalDuration = totalDuration self.currentDctData = DctData(quality: dctQuality) - self.currentDctCoefficients = DctCoefficientsYUVA420(width: width, height: height) } - func getFrame(index: Int, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? { - guard let frameInfo = self.frameMapping[index] else { - return nil + private func loadNextFrame() { + let index: Int + if let currentFrame = self.currentFrame { + if currentFrame.index + 1 >= self.durationMapping.count { + index = 0 + self.compressedDataReader = nil + } else { + index = currentFrame.index + 1 + } + } else { + index = 0 + self.compressedDataReader = nil } - var frameDataOffset = 0 - let frameLength = frameInfo.range.upperBound - frameInfo.range.lowerBound - for i in 0 ..< 4 { - let dctPlane: DctCoefficientPlane - switch i { - case 0: - dctPlane = self.currentDctCoefficients.yPlane - case 1: - dctPlane = self.currentDctCoefficients.uPlane - case 2: - dctPlane = self.currentDctCoefficients.vPlane - case 3: - dctPlane = self.currentDctCoefficients.aPlane - default: - preconditionFailure() + if self.compressedDataReader == nil { + self.compressedDataReader = DecompressedData(compressedData: self.data, dataRange: self.range) + } + + guard let compressedDataReader = self.compressedDataReader else { + self.currentFrame = nil + return + } + + do { + let frameLength = Int(try compressedDataReader.readUInt32()) + + let dctCoefficients: DctCoefficientsYUVA420 + if let sharedDctCoefficients = self.sharedDctCoefficients, sharedDctCoefficients.yPlane.width == self.width, sharedDctCoefficients.yPlane.height == self.height { + dctCoefficients = sharedDctCoefficients + } else { + dctCoefficients = DctCoefficientsYUVA420(width: self.width, height: self.height) + self.sharedDctCoefficients = dctCoefficients } - if frameDataOffset + dctPlane.data.count > frameLength { - break + var frameOffset = 0 + for i in 0 ..< 4 { + let planeLength = Int(try compressedDataReader.readUInt32()) + if planeLength < 0 || planeLength > 20 * 1024 * 1024 { + throw ReadError.generic + } + + let plane: DctCoefficientPlane + switch i { + case 0: + plane = dctCoefficients.yPlane + case 1: + plane = dctCoefficients.uPlane + case 2: + plane = dctCoefficients.vPlane + case 3: + plane = dctCoefficients.aPlane + default: + throw ReadError.generic + } + + if planeLength != plane.data.count { + throw ReadError.generic + } + + if frameOffset + plane.data.count > frameLength { + throw ReadError.generic + } + + try plane.data.withUnsafeMutableBytes { bytes in + try compressedDataReader.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: bytes.count) + } + frameOffset += plane.data.count } - dctPlane.data.withUnsafeMutableBytes { targetBuffer -> Void in - self.data.copyBytes(to: targetBuffer.baseAddress!.assumingMemoryBound(to: UInt8.self), from: (frameInfo.range.lowerBound + frameDataOffset) ..< (frameInfo.range.lowerBound + frameDataOffset + targetBuffer.count)) + self.currentFrame = CurrentFrame(index: index, duration: self.durationMapping[index], dctCoefficients: dctCoefficients) + } catch { + self.currentFrame = nil + self.compressedDataReader = nil + } + } + + func advance(advance: AnimationCacheItem.Advance, requestedFormat: AnimationCacheItemFrame.RequestedFormat) -> AnimationCacheItemFrame? { + switch advance { + case let .frames(count): + for _ in 0 ..< count { + self.loadNextFrame() } - - frameDataOffset += dctPlane.data.count + case let .duration(duration): + var durationOverflow = duration + while true { + if let currentFrame = self.currentFrame { + currentFrame.remainingDuration -= durationOverflow + if currentFrame.remainingDuration <= 0.0 { + durationOverflow = -currentFrame.remainingDuration + self.loadNextFrame() + } else { + break + } + } else { + self.loadNextFrame() + break + } + } + } + + guard let currentFrame = self.currentFrame else { + return nil } let yuvaSurface: ImageYUVA420 @@ -769,13 +786,13 @@ private final class AnimationCacheItemAccessor { if let currentYUVASurface = self.currentYUVASurface { yuvaSurface = currentYUVASurface } else { - yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, rowAlignment: nil) + yuvaSurface = ImageYUVA420(width: currentFrame.dctCoefficients.yPlane.width, height: currentFrame.dctCoefficients.yPlane.height, rowAlignment: nil) } case let .yuva(preferredRowAlignment): - yuvaSurface = ImageYUVA420(width: self.currentDctCoefficients.yPlane.width, height: self.currentDctCoefficients.yPlane.height, rowAlignment: preferredRowAlignment) + yuvaSurface = ImageYUVA420(width: currentFrame.dctCoefficients.yPlane.width, height: currentFrame.dctCoefficients.yPlane.height, rowAlignment: preferredRowAlignment) } - self.currentDctCoefficients.idct(dctData: self.currentDctData, target: yuvaSurface) + currentFrame.dctCoefficients.idct(dctData: self.currentDctData, target: yuvaSurface) switch requestedFormat { case .rgba: @@ -783,7 +800,7 @@ private final class AnimationCacheItemAccessor { yuvaSurface.toARGB(target: currentSurface) self.currentYUVASurface = yuvaSurface - return AnimationCacheItemFrame(format: .rgba(data: currentSurface.argbPlane.data, width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: frameInfo.duration) + return AnimationCacheItemFrame(format: .rgba(data: currentSurface.argbPlane.data, width: currentSurface.argbPlane.width, height: currentSurface.argbPlane.height, bytesPerRow: currentSurface.argbPlane.bytesPerRow), duration: currentFrame.duration) case .yuva: return AnimationCacheItemFrame( format: .yuva( @@ -812,34 +829,10 @@ private final class AnimationCacheItemAccessor { bytesPerRow: yuvaSurface.aPlane.bytesPerRow ) ), - duration: frameInfo.duration + duration: currentFrame.duration ) } } - - func getFrameIndex(duration: Double) -> Int { - if self.totalDuration == 0.0 { - return 0 - } - if self.durationMapping.count <= 1 { - return 0 - } - let normalizedDuration = duration.truncatingRemainder(dividingBy: self.totalDuration) - for i in 1 ..< self.durationMapping.count { - if normalizedDuration < self.durationMapping[i] { - return i - 1 - } - } - return self.durationMapping.count - 1 - } - - func getFrameDuration(index: Int) -> Double? { - if index < self.durationMapping.count { - return self.durationMapping[index] - } else { - return nil - } - } } private func readUInt32(data: Data, offset: Int) -> UInt32 { @@ -884,137 +877,344 @@ private func writeFloat32(data: inout Data, value: Float32) { }) } -private func loadItem(path: String) -> AnimationCacheItem? { - guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else { - return nil +private final class CompressedFileWriter { + enum WriteError: Error { + case generic } - if compressedData.count < 4 { - return nil - } - let decompressedSize = readUInt32(data: compressedData, offset: 0) + private let file: ManagedFile + private let stream: UnsafeMutablePointer - if decompressedSize <= 0 || decompressedSize > 40 * 1024 * 1024 { - return nil - } - guard let data = decompressData(data: compressedData, range: 4 ..< compressedData.count, decompressedSize: Int(decompressedSize)) else { - return nil - } + private let tempBufferSize: Int = 32 * 1024 + private let tempBuffer: UnsafeMutablePointer - let dataLength = data.count + private var didFail: Bool = false - var offset = 0 - - guard dataLength >= offset + 4 else { - return nil - } - let formatVersion = readUInt32(data: data, offset: offset) - offset += 4 - if formatVersion != 2 { - return nil - } - - guard dataLength >= offset + 4 else { - return nil - } - let width = readUInt32(data: data, offset: offset) - offset += 4 - - guard dataLength >= offset + 4 else { - return nil - } - let height = readUInt32(data: data, offset: offset) - offset += 4 - - guard dataLength >= offset + 4 else { - return nil - } - let dctQuality = readUInt32(data: data, offset: offset) - offset += 4 - - guard dataLength >= offset + 4 else { - return nil - } - let frameDataLength = readUInt32(data: data, offset: offset) - offset += 4 - - offset += Int(frameDataLength) - - guard dataLength >= offset + 4 else { - return nil - } - let numFrames = readUInt32(data: data, offset: offset) - offset += 4 - - var frameMapping: [AnimationCacheItemAccessor.FrameInfo] = [] - for _ in 0 ..< Int(numFrames) { - guard dataLength >= offset + 4 + 4 + 4 else { + init?(file: ManagedFile) { + self.file = file + + self.stream = UnsafeMutablePointer.allocate(capacity: 1) + guard compression_stream_init(self.stream, COMPRESSION_STREAM_ENCODE, COMPRESSION_LZFSE) != COMPRESSION_STATUS_ERROR else { + self.stream.deallocate() return nil } - let frameStart = readUInt32(data: data, offset: offset) - offset += 4 - let frameLength = readUInt32(data: data, offset: offset) - offset += 4 - let frameDuration = readFloat32(data: data, offset: offset) - offset += 4 - - frameMapping.append(AnimationCacheItemAccessor.FrameInfo(range: Int(frameStart) ..< Int(frameStart + frameLength), duration: Double(frameDuration))) + self.tempBuffer = UnsafeMutablePointer.allocate(capacity: self.tempBufferSize) } - let itemAccessor = AnimationCacheItemAccessor(data: data, frameMapping: frameMapping, width: Int(width), height: Int(height), dctQuality: Int(dctQuality)) + deinit { + compression_stream_destroy(self.stream) + self.stream.deallocate() + self.tempBuffer.deallocate() + } - return AnimationCacheItem(numFrames: Int(numFrames), getFrame: { index, requestedFormat in - return itemAccessor.getFrame(index: index, requestedFormat: requestedFormat) - }, getFrameIndexImpl: { duration in - return itemAccessor.getFrameIndex(duration: duration) - }, getFrameDurationImpl: { index in - return itemAccessor.getFrameDuration(index: index) + func write(bytes: UnsafePointer, count: Int) throws { + if self.didFail { + throw WriteError.generic + } + + self.stream.pointee.src_ptr = bytes + self.stream.pointee.src_size = count + + while true { + self.stream.pointee.dst_ptr = self.tempBuffer + self.stream.pointee.dst_size = self.tempBufferSize + + let status = compression_stream_process(self.stream, 0) + if status == COMPRESSION_STATUS_ERROR { + self.didFail = true + throw WriteError.generic + } + + let writtenBytes = self.tempBufferSize - self.stream.pointee.dst_size + if writtenBytes > 0 { + let _ = self.file.write(self.tempBuffer, count: writtenBytes) + } + + if status == COMPRESSION_STATUS_END { + break + } else { + if self.stream.pointee.src_size == 0 { + break + } + } + } + } + + func flush() throws { + if self.didFail { + throw WriteError.generic + } + + while true { + self.stream.pointee.dst_ptr = self.tempBuffer + self.stream.pointee.dst_size = self.tempBufferSize + + let status = compression_stream_process(self.stream, Int32(COMPRESSION_STREAM_FINALIZE.rawValue)) + if status == COMPRESSION_STATUS_ERROR { + self.didFail = true + throw WriteError.generic + } + + let writtenBytes = self.tempBufferSize - self.stream.pointee.dst_size + if writtenBytes > 0 { + let _ = self.file.write(self.tempBuffer, count: writtenBytes) + } + + if status == COMPRESSION_STATUS_END { + break + } + } + } + + func writeUInt32(_ value: UInt32) throws { + var value: UInt32 = value + try withUnsafeBytes(of: &value, { bytes -> Void in + try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + } + + func writeFloat32(_ value: Float32) throws { + var value: Float32 = value + try withUnsafeBytes(of: &value, { bytes -> Void in + try self.write(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + } +} + +private final class DecompressedData { + enum ReadError: Error { + case didReadToEnd + } + + private let compressedData: Data + private let dataRange: Range + private let stream: UnsafeMutablePointer + private var isComplete = false + + init?(compressedData: Data, dataRange: Range) { + self.compressedData = compressedData + self.dataRange = dataRange + + self.stream = UnsafeMutablePointer.allocate(capacity: 1) + guard compression_stream_init(self.stream, COMPRESSION_STREAM_DECODE, COMPRESSION_LZFSE) != COMPRESSION_STATUS_ERROR else { + self.stream.deallocate() + return nil + } + + self.compressedData.withUnsafeBytes { bytes in + self.stream.pointee.src_ptr = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: dataRange.lowerBound) + self.stream.pointee.src_size = dataRange.upperBound - dataRange.lowerBound + } + } + + deinit { + compression_stream_destroy(self.stream) + self.stream.deallocate() + } + + func read(bytes: UnsafeMutablePointer, count: Int) throws { + if self.isComplete { + throw ReadError.didReadToEnd + } + + self.stream.pointee.dst_ptr = bytes + self.stream.pointee.dst_size = count + + let status = compression_stream_process(self.stream, 0) + + if status == COMPRESSION_STATUS_ERROR { + self.isComplete = true + throw ReadError.didReadToEnd + } else if status == COMPRESSION_STATUS_END { + if self.stream.pointee.src_size == 0 { + self.isComplete = true + } + } + + if self.stream.pointee.dst_size != 0 { + throw ReadError.didReadToEnd + } + } + + func readUInt32() throws -> UInt32 { + var value: UInt32 = 0 + try withUnsafeMutableBytes(of: &value, { bytes -> Void in + try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + return value + } + + func readFloat32() throws -> Float32 { + var value: Float32 = 0 + try withUnsafeMutableBytes(of: &value, { bytes -> Void in + try self.read(bytes: bytes.baseAddress!.assumingMemoryBound(to: UInt8.self), count: 4) + }) + return value + } +} + +private enum LoadItemError: Error { + case dataError +} + +private func loadItem(path: String) throws -> AnimationCacheItem { + guard let compressedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped) else { + throw LoadItemError.dataError + } + + var offset: Int = 0 + let dataLength = compressedData.count + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let formatVersion = readUInt32(data: compressedData, offset: offset) + offset += 4 + if formatVersion != 3 { + throw LoadItemError.dataError + } + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let width = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let height = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let dctQuality = readUInt32(data: compressedData, offset: offset) + offset += 4 + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let contentLength = Int(readUInt32(data: compressedData, offset: offset)) + offset += 4 + + let compressedFrameDataRange = offset ..< (offset + contentLength) + offset += contentLength + + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let frameCount = Int(readUInt32(data: compressedData, offset: offset)) + offset += 4 + + var frameMapping: [AnimationCacheItemAccessor.FrameInfo] = [] + for _ in 0 ..< frameCount { + if offset + 4 > dataLength { + throw LoadItemError.dataError + } + let frameDuration = readFloat32(data: compressedData, offset: offset) + offset += 4 + + frameMapping.append(AnimationCacheItemAccessor.FrameInfo(duration: Double(frameDuration))) + } + + let itemAccessor = AnimationCacheItemAccessor(data: compressedData, range: compressedFrameDataRange, frameMapping: frameMapping, width: Int(width), height: Int(height), dctQuality: Int(dctQuality)) + + return AnimationCacheItem(numFrames: frameMapping.count, advanceImpl: { advance, requestedFormat in + return itemAccessor.advance(advance: advance, requestedFormat: requestedFormat) }) } private func adaptItemFromHigherResolution(itemPath: String, width: Int, height: Int, itemDirectoryPath: String, higherResolutionPath: String, allocateTempFile: @escaping () -> String) -> AnimationCacheItem? { - guard let higherResolutionItem = loadItem(path: higherResolutionPath) else { + guard let higherResolutionItem = try? loadItem(path: higherResolutionPath) else { return nil } guard let writer = AnimationCacheItemWriterInternal(allocateTempFile: allocateTempFile) else { return nil } - for i in 0 ..< higherResolutionItem.numFrames { - guard let duration = higherResolutionItem.getFrameDuration(index: i) else { - break + do { + for _ in 0 ..< higherResolutionItem.numFrames { + try writer.add(with: { yuva in + guard let frame = higherResolutionItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else { + return nil + } + switch frame.format { + case .rgba: + return nil + case let .yuva(y, u, v, a): + yuva.yPlane.copyScaled(fromPlane: y) + yuva.uPlane.copyScaled(fromPlane: u) + yuva.vPlane.copyScaled(fromPlane: v) + yuva.aPlane.copyScaled(fromPlane: a) + } + + return frame.duration + }, proposedWidth: width, proposedHeight: height) } - writer.add(with: { yuva in - guard let frame = higherResolutionItem.getFrame(index: i, requestedFormat: .yuva(rowAlignment: yuva.yPlane.rowAlignment)) else { - return + + let result = try writer.finish() + + guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { + return nil + } + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.path, toPath: itemPath) else { + return nil + } + guard let item = try? loadItem(path: itemPath) else { + return nil + } + return item + } catch { + return nil + } +} + +private func generateFirstFrameFromItem(itemPath: String, animationItemPath: String, allocateTempFile: @escaping () -> String) -> Bool { + guard let animationItem = try? loadItem(path: animationItemPath) else { + return false + } + guard let writer = AnimationCacheItemWriterInternal(allocateTempFile: allocateTempFile) else { + return false + } + + do { + for _ in 0 ..< min(1, animationItem.numFrames) { + guard let frame = animationItem.advance(advance: .frames(1), requestedFormat: .yuva(rowAlignment: 1)) else { + return false } switch frame.format { case .rgba: - return + return false case let .yuva(y, u, v, a): - yuva.yPlane.copyScaled(fromPlane: y) - yuva.uPlane.copyScaled(fromPlane: u) - yuva.vPlane.copyScaled(fromPlane: v) - yuva.aPlane.copyScaled(fromPlane: a) + try writer.add(with: { yuva in + assert(yuva.yPlane.bytesPerRow == y.bytesPerRow) + assert(yuva.uPlane.bytesPerRow == u.bytesPerRow) + assert(yuva.vPlane.bytesPerRow == v.bytesPerRow) + assert(yuva.aPlane.bytesPerRow == a.bytesPerRow) + + yuva.yPlane.copyScaled(fromPlane: y) + yuva.uPlane.copyScaled(fromPlane: u) + yuva.vPlane.copyScaled(fromPlane: v) + yuva.aPlane.copyScaled(fromPlane: a) + + return frame.duration + }, proposedWidth: y.width, proposedHeight: y.height) } - }, proposedWidth: width, proposedHeight: height, duration: duration) + } + + let result = try writer.finish() + + let _ = try? FileManager.default.removeItem(atPath: itemPath) + guard let _ = try? FileManager.default.moveItem(atPath: result.path, toPath: itemPath) else { + return false + } + return true + } catch { + return false } - - guard let result = writer.finish() else { - return nil - } - guard let _ = try? FileManager.default.createDirectory(at: URL(fileURLWithPath: itemDirectoryPath), withIntermediateDirectories: true, attributes: nil) else { - return nil - } - let _ = try? FileManager.default.removeItem(atPath: itemPath) - guard let _ = try? FileManager.default.moveItem(atPath: result.path, toPath: itemPath) else { - return nil - } - guard let item = loadItem(path: itemPath) else { - return nil - } - return item } private func findHigherResolutionFileForAdaptation(itemDirectoryPath: String, baseName: String, baseSuffix: String, width: Int, height: Int) -> String? { @@ -1106,7 +1306,7 @@ public final class AnimationCacheImpl: AnimationCache { let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - if FileManager.default.fileExists(atPath: itemPath), let item = loadItem(path: itemPath) { + if FileManager.default.fileExists(atPath: itemPath), let item = try? loadItem(path: itemPath) { updateResult(AnimationCacheItemResult(item: item, isFinal: true)) return EmptyDisposable @@ -1130,6 +1330,7 @@ public final class AnimationCacheImpl: AnimationCache { if beginFetch { let fetchQueueIndex = self.nextFetchQueueIndex self.nextFetchQueueIndex += 1 + let allocateTempFile = self.allocateTempFile guard let writer = AnimationCacheItemWriterImpl(queue: self.fetchQueues[fetchQueueIndex % self.fetchQueues.count], allocateTempFile: self.allocateTempFile, completion: { [weak self, weak itemContext] result in queue.async { guard let strongSelf = self, let itemContext = itemContext, itemContext === strongSelf.itemContexts[sourceId] else { @@ -1148,11 +1349,10 @@ public final class AnimationCacheImpl: AnimationCache { guard let _ = try? FileManager.default.moveItem(atPath: result.animationPath, toPath: itemPath) else { return } - let _ = try? FileManager.default.removeItem(atPath: itemFirstFramePath) - guard let _ = try? FileManager.default.moveItem(atPath: result.firstFramePath, toPath: itemFirstFramePath) else { - return - } - guard let item = loadItem(path: itemPath) else { + + let _ = generateFirstFrameFromItem(itemPath: itemFirstFramePath, animationItemPath: itemPath, allocateTempFile: allocateTempFile) + + guard let item = try? loadItem(path: itemPath) else { return } @@ -1197,7 +1397,7 @@ public final class AnimationCacheImpl: AnimationCache { let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" if FileManager.default.fileExists(atPath: itemFirstFramePath) { - return loadItem(path: itemFirstFramePath) + return try? loadItem(path: itemFirstFramePath) } if let adaptationItemPath = findHigherResolutionFileForAdaptation(itemDirectoryPath: itemDirectoryPath, baseName: "\(hashString)_", baseSuffix: "-f", width: Int(size.width), height: Int(size.height)) { @@ -1215,7 +1415,7 @@ public final class AnimationCacheImpl: AnimationCache { let itemDirectoryPath = "\(basePath)/\(sourceIdPath.directory)" let itemFirstFramePath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)-f" - if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = loadItem(path: itemFirstFramePath) { + if FileManager.default.fileExists(atPath: itemFirstFramePath), let item = try? loadItem(path: itemFirstFramePath) { completion(item) return EmptyDisposable } diff --git a/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift index 9965a8ca92..d4cf07ff4e 100644 --- a/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift +++ b/submodules/TelegramUI/Components/AudioTranscriptionButtonComponent/Sources/AudioTranscriptionButtonComponent.swift @@ -110,18 +110,18 @@ public final class AudioTranscriptionButtonComponent: Component { component: AnyComponent(LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: animationName, - colors: [ - "icon.Group 3.Stroke 1": foregroundColor, - "icon.Group 1.Stroke 1": foregroundColor, - "icon.Group 4.Stroke 1": foregroundColor, - "icon.Group 2.Stroke 1": foregroundColor, - "Artboard Copy 2 Outlines.Group 5.Stroke 1": foregroundColor, - "Artboard Copy 2 Outlines.Group 1.Stroke 1": foregroundColor, - "Artboard Copy 2 Outlines.Group 4.Stroke 1": foregroundColor, - "Artboard Copy Outlines.Group 1.Stroke 1": foregroundColor, - ], mode: .animateTransitionFromPrevious ), + colors: [ + "icon.Group 3.Stroke 1": foregroundColor, + "icon.Group 1.Stroke 1": foregroundColor, + "icon.Group 4.Stroke 1": foregroundColor, + "icon.Group 2.Stroke 1": foregroundColor, + "Artboard Copy 2 Outlines.Group 5.Stroke 1": foregroundColor, + "Artboard Copy 2 Outlines.Group 1.Stroke 1": foregroundColor, + "Artboard Copy 2 Outlines.Group 4.Stroke 1": foregroundColor, + "Artboard Copy Outlines.Group 1.Stroke 1": foregroundColor, + ], size: CGSize(width: 30.0, height: 30.0) )), environment: {}, @@ -142,11 +142,11 @@ public final class AudioTranscriptionButtonComponent: Component { component: AnyComponent(LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "voicets_progress", - colors: [ - "Rectangle 60.Rectangle 60.Stroke 1": foregroundColor - ], mode: .animating(loop: true) ), + colors: [ + "Rectangle 60.Rectangle 60.Stroke 1": foregroundColor + ], size: progressFrame.size )), environment: {}, diff --git a/submodules/TelegramUI/Components/AudioTranscriptionPendingIndicatorComponent/Sources/AudioTranscriptionPendingIndicatorComponent.swift b/submodules/TelegramUI/Components/AudioTranscriptionPendingIndicatorComponent/Sources/AudioTranscriptionPendingIndicatorComponent.swift index ff7e3521df..e45f38fbbb 100644 --- a/submodules/TelegramUI/Components/AudioTranscriptionPendingIndicatorComponent/Sources/AudioTranscriptionPendingIndicatorComponent.swift +++ b/submodules/TelegramUI/Components/AudioTranscriptionPendingIndicatorComponent/Sources/AudioTranscriptionPendingIndicatorComponent.swift @@ -142,13 +142,13 @@ public final class AudioTranscriptionPendingLottieIndicatorComponent: Component component: AnyComponent(LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "animated_text_dots", - colors: [ - "Comp 1.Point 3.Group 1.Fill 1": component.color, - "Comp 1.Point 2.Group 1.Fill 1": component.color, - "Comp 1.Point 1.Group 1.Fill 1": component.color - ], mode: .animating(loop: true) ), + colors: [ + "Comp 1.Point 3.Group 1.Fill 1": component.color, + "Comp 1.Point 2.Group 1.Fill 1": component.color, + "Comp 1.Point 1.Group 1.Fill 1": component.color + ], size: animationSize )), environment: {}, diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index be5edb1358..7b1bf8d1a9 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -26,16 +26,30 @@ import AudioToolbox import SolidRoundedButtonComponent private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white) +private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white) +private let lockedBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeLock"), color: .white) private final class PremiumBadgeView: UIView { + private var badge: EmojiPagerContentComponent.View.ItemLayer.Badge? + + let contentLayer: SimpleLayer + private let overlayColorLayer: SimpleLayer private let iconLayer: SimpleLayer init() { + self.contentLayer = SimpleLayer() + self.contentLayer.contentsGravity = .resize + self.contentLayer.masksToBounds = true + + self.overlayColorLayer = SimpleLayer() + self.overlayColorLayer.masksToBounds = true + self.iconLayer = SimpleLayer() - self.iconLayer.contents = premiumBadgeIcon?.cgImage super.init(frame: CGRect()) + self.layer.addSublayer(self.contentLayer) + self.layer.addSublayer(self.overlayColorLayer) self.layer.addSublayer(self.iconLayer) } @@ -43,27 +57,62 @@ private final class PremiumBadgeView: UIView { fatalError("init(coder:) has not been implemented") } - func update(backgroundColor: UIColor, size: CGSize) { - //self.updateColor(color: backgroundColor, transition: .immediate) - self.backgroundColor = backgroundColor - self.layer.cornerRadius = size.width / 2.0 + func update(transition: Transition, badge: EmojiPagerContentComponent.View.ItemLayer.Badge, backgroundColor: UIColor, size: CGSize) { + if self.badge != badge { + self.badge = badge + + switch badge { + case .premium: + self.iconLayer.contents = premiumBadgeIcon?.cgImage + case .featured: + self.iconLayer.contents = featuredBadgeIcon?.cgImage + case .locked: + self.iconLayer.contents = lockedBadgeIcon?.cgImage + } + } - self.iconLayer.frame = CGRect(origin: CGPoint(), size: size).insetBy(dx: 2.0, dy: 2.0) + let iconInset: CGFloat + switch badge { + case .premium: + iconInset = 2.0 + case .featured: + iconInset = 0.0 + case .locked: + iconInset = 0.0 + } - //super.update(size: size, cornerRadius: min(size.width / 2.0, size.height / 2.0), transition: .immediate) + self.overlayColorLayer.backgroundColor = backgroundColor.cgColor + + transition.setFrame(layer: self.contentLayer, frame: CGRect(origin: CGPoint(), size: size)) + transition.setCornerRadius(layer: self.contentLayer, cornerRadius: min(size.width / 2.0, size.height / 2.0)) + + transition.setFrame(layer: self.overlayColorLayer, frame: CGRect(origin: CGPoint(), size: size)) + 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)) } } private final class GroupHeaderLayer: SimpleLayer { + private let textLayer: SimpleLayer private var lockIconLayer: SimpleLayer? + private(set) var clearIconLayer: SimpleLayer? + + private var theme: PresentationTheme? private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)? override init() { + self.textLayer = SimpleLayer() + super.init() + + self.addSublayer(self.textLayer) } override init(layer: Any) { + self.textLayer = SimpleLayer() + super.init(layer: layer) } @@ -71,11 +120,17 @@ private final class GroupHeaderLayer: SimpleLayer { fatalError("init(coder:) has not been implemented") } - func update(theme: PresentationTheme, title: String, isPremium: Bool, constrainedWidth: CGFloat) -> (size: CGSize, horizontalOffset: CGFloat) { + func update(theme: PresentationTheme, title: String, isPremiumLocked: Bool, hasClear: Bool, constrainedWidth: CGFloat) -> CGSize { + var themeUpdated = false + if self.theme !== theme { + self.theme = theme + themeUpdated = true + } + let color = theme.chat.inputMediaPanel.stickersSectionTextColor - let horizontalOffset: CGFloat - if isPremium { + let titleHorizontalOffset: CGFloat + if isPremiumLocked { let lockIconLayer: SimpleLayer if let current = self.lockIconLayer { lockIconLayer = current @@ -87,38 +142,67 @@ private final class GroupHeaderLayer: SimpleLayer { if let image = PresentationResourcesChat.chatEntityKeyboardLock(theme) { let imageSize = image.size.aspectFitted(CGSize(width: 16.0, height: 16.0)) lockIconLayer.contents = image.cgImage - horizontalOffset = imageSize.width + 2.0 - lockIconLayer.frame = CGRect(origin: CGPoint(x: -imageSize.width - 2.0, y: 0.0), size: imageSize) + titleHorizontalOffset = imageSize.width + 2.0 + lockIconLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: imageSize) } else { lockIconLayer.contents = nil - horizontalOffset = 0.0 + titleHorizontalOffset = 0.0 } } else { if let lockIconLayer = self.lockIconLayer { self.lockIconLayer = nil lockIconLayer.removeFromSuperlayer() } - horizontalOffset = 0.0 + titleHorizontalOffset = 0.0 } - if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == constrainedWidth { - return (currentTextLayout.size, horizontalOffset) + let textConstrainedWidth = constrainedWidth - titleHorizontalOffset - 10.0 + + let textSize: CGSize + if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == textConstrainedWidth { + textSize = currentTextLayout.size + } else { + let string = NSAttributedString(string: title.uppercased(), font: Font.medium(12.0), textColor: color) + let stringBounds = string.boundingRect(with: CGSize(width: textConstrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil) + textSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height)) + self.textLayer.contents = generateImage(textSize, opaque: false, scale: 0.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + + string.draw(in: stringBounds) + + UIGraphicsPopContext() + })?.cgImage + self.currentTextLayout = (title, color, textConstrainedWidth, textSize) } - let string = NSAttributedString(string: title.uppercased(), font: Font.medium(12.0), textColor: color) - let stringBounds = string.boundingRect(with: CGSize(width: constrainedWidth, height: 100.0), options: .usesLineFragmentOrigin, context: nil) - let size = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height)) - self.contents = generateImage(size, opaque: false, scale: 0.0, rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - UIGraphicsPushContext(context) - - string.draw(in: stringBounds) - - UIGraphicsPopContext() - })?.cgImage - self.currentTextLayout = (title, color, constrainedWidth, size) + self.textLayer.frame = CGRect(origin: CGPoint(x: titleHorizontalOffset, y: 0.0), size: textSize) - return (size, horizontalOffset) + var clearWidth: CGFloat = 0.0 + if hasClear { + let clearIconLayer: SimpleLayer + var updateImage = themeUpdated + if let current = self.clearIconLayer { + clearIconLayer = current + } else { + updateImage = true + clearIconLayer = SimpleLayer() + self.clearIconLayer = clearIconLayer + self.addSublayer(clearIconLayer) + } + var clearSize = clearIconLayer.bounds.size + if updateImage, let image = PresentationResourcesChat.chatInputMediaPanelGridDismissImage(theme) { + clearSize = image.size + clearSize.width = 10.0 + clearSize.height = 10.0 + clearIconLayer.contents = image.cgImage + } + + clearIconLayer.frame = CGRect(origin: CGPoint(x: titleHorizontalOffset + textSize.width + 4.0, y: floorToScreenPixels((textSize.height - clearSize.height) / 2.0)), size: clearSize) + clearWidth = 4.0 + clearSize.width + } + + return CGSize(width: titleHorizontalOffset + textSize.width + clearWidth, height: textSize.height) } } @@ -129,7 +213,8 @@ public final class EmojiPagerContentComponent: Component { public let performItemAction: (Item, UIView, CGRect, CALayer) -> Void public let deleteBackwards: () -> Void public let openStickerSettings: () -> Void - public let openPremiumSection: () -> Void + public let addGroupAction: (AnyHashable, Bool) -> Void + public let clearGroup: (AnyHashable) -> Void public let pushController: (ViewController) -> Void public let presentController: (ViewController) -> Void public let presentGlobalOverlayController: (ViewController) -> Void @@ -141,7 +226,8 @@ public final class EmojiPagerContentComponent: Component { performItemAction: @escaping (Item, UIView, CGRect, CALayer) -> Void, deleteBackwards: @escaping () -> Void, openStickerSettings: @escaping () -> Void, - openPremiumSection: @escaping () -> Void, + addGroupAction: @escaping (AnyHashable, Bool) -> Void, + clearGroup: @escaping (AnyHashable) -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController) -> Void, presentGlobalOverlayController: @escaping (ViewController) -> Void, @@ -152,7 +238,8 @@ public final class EmojiPagerContentComponent: Component { self.performItemAction = performItemAction self.deleteBackwards = deleteBackwards self.openStickerSettings = openStickerSettings - self.openPremiumSection = openPremiumSection + self.addGroupAction = addGroupAction + self.clearGroup = clearGroup self.pushController = pushController self.presentController = presentController self.presentGlobalOverlayController = presentGlobalOverlayController @@ -210,7 +297,9 @@ public final class EmojiPagerContentComponent: Component { public let supergroupId: AnyHashable public let groupId: AnyHashable public let title: String? - public let isPremium: Bool + public let isFeatured: Bool + public let isPremiumLocked: Bool + public let hasClear: Bool public let displayPremiumBadges: Bool public let items: [Item] @@ -218,19 +307,26 @@ public final class EmojiPagerContentComponent: Component { supergroupId: AnyHashable, groupId: AnyHashable, title: String?, - isPremium: Bool, + isFeatured: Bool, + isPremiumLocked: Bool, + hasClear: Bool, displayPremiumBadges: Bool, items: [Item] ) { self.supergroupId = supergroupId self.groupId = groupId self.title = title - self.isPremium = isPremium + self.isFeatured = isFeatured + self.isPremiumLocked = isPremiumLocked + self.hasClear = hasClear self.displayPremiumBadges = displayPremiumBadges self.items = items } public static func ==(lhs: ItemGroup, rhs: ItemGroup) -> Bool { + if lhs === rhs { + return true + } if lhs.supergroupId != rhs.supergroupId { return false } @@ -240,7 +336,13 @@ public final class EmojiPagerContentComponent: Component { if lhs.title != rhs.title { return false } - if lhs.isPremium != rhs.isPremium { + if lhs.isFeatured != rhs.isFeatured { + return false + } + if lhs.isPremiumLocked != rhs.isPremiumLocked { + return false + } + if lhs.hasClear != rhs.hasClear { return false } if lhs.displayPremiumBadges != rhs.displayPremiumBadges { @@ -323,7 +425,8 @@ public final class EmojiPagerContentComponent: Component { let supergroupId: AnyHashable let groupId: AnyHashable let hasTitle: Bool - let isPremium: Bool + let isPremiumLocked: Bool + let isFeatured: Bool let itemCount: Int } @@ -392,7 +495,7 @@ public final class EmojiPagerContentComponent: Component { let numRowsInGroup = (itemGroup.itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow var groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.visibleItemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) - if itemGroup.isPremium { + if itemGroup.isPremiumLocked || itemGroup.isFeatured { groupContentSize.height += self.premiumButtonInset + self.premiumButtonHeight } self.itemGroupLayouts.append(ItemGroupLayout( @@ -426,8 +529,8 @@ public final class EmojiPagerContentComponent: Component { ) } - func visibleItems(for rect: CGRect) -> [(supergroupId: AnyHashable, groupId: AnyHashable, groupIndex: Int, groupItems: Range)] { - var result: [(supergroupId: AnyHashable, groupId: AnyHashable, groupIndex: Int, groupItems: Range)] = [] + func visibleItems(for rect: CGRect) -> [(supergroupId: AnyHashable, groupId: AnyHashable, groupIndex: Int, groupItems: Range?)] { + var result: [(supergroupId: AnyHashable, groupId: AnyHashable, groupIndex: Int, groupItems: Range?)] = [] for groupIndex in 0 ..< self.itemGroupLayouts.count { let group = self.itemGroupLayouts[groupIndex] @@ -443,14 +546,12 @@ public final class EmojiPagerContentComponent: Component { let minVisibleIndex = minVisibleRow * self.itemsPerRow let maxVisibleIndex = min(group.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1) - if maxVisibleIndex >= minVisibleIndex { - result.append(( - supergroupId: group.supergroupId, - groupId: group.groupId, - groupIndex: groupIndex, - groupItems: minVisibleIndex ..< (maxVisibleIndex + 1) - )) - } + result.append(( + supergroupId: group.supergroupId, + groupId: group.groupId, + groupIndex: groupIndex, + groupItems: maxVisibleIndex >= minVisibleIndex ? (minVisibleIndex ..< (maxVisibleIndex + 1)) : nil + )) } return result @@ -518,6 +619,12 @@ public final class EmojiPagerContentComponent: Component { var staticEmoji: String? } + enum Badge { + case premium + case locked + case featured + } + let item: Item private let file: TelegramMediaFile? @@ -528,6 +635,9 @@ public final class EmojiPagerContentComponent: Component { private var fetchDisposable: Disposable? private var premiumBadgeView: PremiumBadgeView? + private var badge: Badge? + private var validSize: CGSize? + private var isInHierarchyValue: Bool = false public var isVisibleForAnimations: Bool = false { didSet { @@ -549,7 +659,6 @@ public final class EmojiPagerContentComponent: Component { renderer: MultiAnimationRenderer, placeholderColor: UIColor, blurredBadgeColor: UIColor, - displayPremiumBadgeIfAvailable: Bool, pointSize: CGSize, onUpdateDisplayPlaceholder: @escaping (Bool, Double) -> Void ) { @@ -640,20 +749,11 @@ public final class EmojiPagerContentComponent: Component { self.fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: isSmall)).start() } - - if displayPremiumBadgeIfAvailable && file.isPremiumSticker { - let premiumBadgeView = PremiumBadgeView() - let badgeSize = CGSize(width: 20.0, height: 20.0) - premiumBadgeView.frame = CGRect(origin: CGPoint(x: pointSize.width - badgeSize.width, y: pointSize.height - badgeSize.height), size: badgeSize) - premiumBadgeView.update(backgroundColor: blurredBadgeColor, size: badgeSize) - self.premiumBadgeView = premiumBadgeView - self.addSublayer(premiumBadgeView.layer) - } } else if let staticEmoji = staticEmoji { let image = generateImage(self.size, opaque: false, scale: min(UIScreenScale, 3.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) - let preScaleFactor: CGFloat = 1.3 + let preScaleFactor: CGFloat = 2.0 let scaledSize = CGSize(width: floor(size.width * preScaleFactor), height: floor(size.height * preScaleFactor)) let scaleFactor = scaledSize.width / size.width @@ -705,6 +805,42 @@ public final class EmojiPagerContentComponent: Component { return nullAction } + func update(transition: Transition, size: CGSize, badge: Badge?, blurredBadgeColor: UIColor, blurredBadgeBackgroundColor: UIColor) { + if self.badge != badge || self.validSize != size { + self.badge = badge + self.validSize = size + + if let badge = badge { + var badgeTransition = transition + let premiumBadgeView: PremiumBadgeView + if let current = self.premiumBadgeView { + premiumBadgeView = current + } else { + badgeTransition = .immediate + premiumBadgeView = PremiumBadgeView() + self.premiumBadgeView = premiumBadgeView + self.addSublayer(premiumBadgeView.layer) + } + + let badgeDiameter = min(16.0, floor(size.height * 0.5)) + let badgeSize = CGSize(width: badgeDiameter, height: badgeDiameter) + badgeTransition.setFrame(view: premiumBadgeView, frame: CGRect(origin: CGPoint(x: size.width - badgeSize.width, y: size.height - badgeSize.height), size: badgeSize)) + premiumBadgeView.update(transition: badgeTransition, badge: badge, backgroundColor: blurredBadgeColor, size: badgeSize) + + self.blurredRepresentationBackgroundColor = blurredBadgeBackgroundColor + self.blurredRepresentationTarget = premiumBadgeView.contentLayer + } else { + if let premiumBadgeView = self.premiumBadgeView { + self.premiumBadgeView = nil + premiumBadgeView.removeFromSuperview() + + self.blurredRepresentationBackgroundColor = nil + self.blurredRepresentationTarget = nil + } + } + } + } + private func updatePlayback() { let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations @@ -772,7 +908,7 @@ public final class EmojiPagerContentComponent: Component { private weak var state: EmptyComponentState? private var pagerEnvironment: PagerComponentChildEnvironment? private var theme: PresentationTheme? - private var activeItemUpdated: ActionSlot<(AnyHashable, Transition)>? + private var activeItemUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)>? private var itemLayout: ItemLayout? private var peekRecognizer: PeekControllerGestureRecognizer? @@ -985,7 +1121,7 @@ public final class EmojiPagerContentComponent: Component { } public func scrollToItemGroup(id supergroupId: AnyHashable, subgroupId: Int32?) { - guard let component = self.component, let itemLayout = self.itemLayout else { + guard let component = self.component, let pagerEnvironment = self.pagerEnvironment, let itemLayout = self.itemLayout else { return } for groupIndex in 0 ..< itemLayout.itemGroupLayouts.count { @@ -1009,7 +1145,6 @@ public final class EmojiPagerContentComponent: Component { let wasIgnoringScrollingEvents = self.ignoreScrolling self.ignoreScrolling = true self.scrollView.setContentOffset(self.scrollView.contentOffset, animated: false) - self.ignoreScrolling = wasIgnoringScrollingEvents self.keepTopPanelVisibleUntilScrollingInput = true @@ -1020,7 +1155,344 @@ public final class EmojiPagerContentComponent: Component { anchorFrame = group.frame } - self.scrollView.scrollRectToVisible(CGRect(origin: anchorFrame.origin.offsetBy(dx: 0.0, dy: floor(-itemLayout.verticalGroupSpacing / 2.0) - 41.0), size: CGSize(width: 1.0, height: self.scrollView.bounds.height)), animated: true) + var scrollPosition = anchorFrame.minY + floor(-itemLayout.verticalGroupSpacing / 2.0) - pagerEnvironment.containerInsets.top + if scrollPosition > self.scrollView.contentSize.height - self.scrollView.bounds.height { + scrollPosition = self.scrollView.contentSize.height - self.scrollView.bounds.height + } + if scrollPosition < 0.0 { + scrollPosition = 0.0 + } + + let offsetDirectionSign: Double = scrollPosition < self.scrollView.bounds.minY ? -1.0 : 1.0 + + var previousVisibleLayers: [ItemLayer.Key: (CALayer, CGRect)] = [:] + for (id, layer) in self.visibleItemLayers { + previousVisibleLayers[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) + } + var previousVisiblePlaceholderViews: [ItemLayer.Key: (UIView, CGRect)] = [:] + for (id, view) in self.visibleItemPlaceholderViews { + previousVisiblePlaceholderViews[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) + } + var previousVisibleGroupHeaders: [AnyHashable: (CALayer, CGRect)] = [:] + for (id, layer) in self.visibleGroupHeaders { + if !self.scrollView.bounds.intersects(layer.frame) { + continue + } + previousVisibleGroupHeaders[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) + } + var previousVisibleGroupBorders: [AnyHashable: (CALayer, CGRect)] = [:] + for (id, layer) in self.visibleGroupBorders { + previousVisibleGroupBorders[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) + } + var previousVisibleGroupPremiumButtons: [AnyHashable: (UIView, CGRect)] = [:] + for (id, view) in self.visibleGroupPremiumButtons { + if let view = view.view { + previousVisibleGroupPremiumButtons[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) + } + } + + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: scrollPosition), size: self.scrollView.bounds.size) + self.ignoreScrolling = wasIgnoringScrollingEvents + + self.updateVisibleItems(transition: .immediate, attemptSynchronousLoads: true) + + var commonItemOffset: CGFloat? + var previousVisibleBoundingRect: CGRect? + for (id, layerAndFrame) in previousVisibleLayers { + if let layer = self.visibleItemLayers[id] { + if commonItemOffset == nil { + let visibleFrame = layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + commonItemOffset = layerAndFrame.1.minY - visibleFrame.minY + } + break + } else { + if let previousVisibleBoundingRectValue = previousVisibleBoundingRect { + previousVisibleBoundingRect = layerAndFrame.1.union(previousVisibleBoundingRectValue) + } else { + previousVisibleBoundingRect = layerAndFrame.1 + } + } + } + + for (id, viewAndFrame) in previousVisiblePlaceholderViews { + if let view = self.visibleItemPlaceholderViews[id] { + if commonItemOffset == nil { + let visibleFrame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + commonItemOffset = viewAndFrame.1.minY - visibleFrame.minY + } + break + } else { + if let previousVisibleBoundingRectValue = previousVisibleBoundingRect { + previousVisibleBoundingRect = viewAndFrame.1.union(previousVisibleBoundingRectValue) + } else { + previousVisibleBoundingRect = viewAndFrame.1 + } + } + } + + for (id, layerAndFrame) in previousVisibleGroupHeaders { + if let layer = self.visibleGroupHeaders[id] { + if commonItemOffset == nil, self.scrollView.bounds.intersects(layer.frame) { + let visibleFrame = layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + commonItemOffset = layerAndFrame.1.minY - visibleFrame.minY + } + break + } else { + if let previousVisibleBoundingRectValue = previousVisibleBoundingRect { + previousVisibleBoundingRect = layerAndFrame.1.union(previousVisibleBoundingRectValue) + } else { + previousVisibleBoundingRect = layerAndFrame.1 + } + } + } + + /*for (id, layerAndFrame) in previousVisibleGroupBorders { + if let layer = self.visibleGroupBorders[id] { + if commonItemOffset == nil, self.scrollView.bounds.intersects(layer.frame) { + let visibleFrame = layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + commonItemOffset = layerAndFrame.1.minY - visibleFrame.minY + } + break + } else { + if let previousVisibleBoundingRectValue = previousVisibleBoundingRect { + previousVisibleBoundingRect = layerAndFrame.1.union(previousVisibleBoundingRectValue) + } else { + previousVisibleBoundingRect = layerAndFrame.1 + } + } + }*/ + + for (id, viewAndFrame) in previousVisibleGroupPremiumButtons { + if let view = self.visibleGroupPremiumButtons[id]?.view, self.scrollView.bounds.intersects(view.frame) { + if commonItemOffset == nil { + let visibleFrame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + commonItemOffset = viewAndFrame.1.minY - visibleFrame.minY + } + break + } else { + if let previousVisibleBoundingRectValue = previousVisibleBoundingRect { + previousVisibleBoundingRect = viewAndFrame.1.union(previousVisibleBoundingRectValue) + } else { + previousVisibleBoundingRect = viewAndFrame.1 + } + } + } + + let duration = 0.4 + let timingFunction = kCAMediaTimingFunctionSpring + + if let commonItemOffset = commonItemOffset { + for (_, layer) in self.visibleItemLayers { + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, layerAndFrame) in previousVisibleLayers { + if self.visibleItemLayers[id] != nil { + continue + } + let layer = layerAndFrame.0 + self.scrollView.layer.addSublayer(layer) + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in + layer?.removeFromSuperlayer() + }) + } + + for (_, view) in self.visibleItemPlaceholderViews { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, viewAndFrame) in previousVisiblePlaceholderViews { + if self.visibleItemPlaceholderViews[id] != nil { + continue + } + let view = viewAndFrame.0 + self.placeholdersContainerView.addSubview(view) + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + + for (_, layer) in self.visibleGroupHeaders { + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, layerAndFrame) in previousVisibleGroupHeaders { + if self.visibleGroupHeaders[id] != nil { + continue + } + let layer = layerAndFrame.0 + self.scrollView.layer.addSublayer(layer) + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in + layer?.removeFromSuperlayer() + }) + } + + for (_, layer) in self.visibleGroupBorders { + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, layerAndFrame) in previousVisibleGroupBorders { + if self.visibleGroupBorders[id] != nil { + continue + } + let layer = layerAndFrame.0 + self.scrollView.layer.addSublayer(layer) + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in + layer?.removeFromSuperlayer() + }) + } + + for (_, view) in self.visibleGroupPremiumButtons { + if let view = view.view { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + } + for (id, viewAndFrame) in previousVisibleGroupPremiumButtons { + if self.visibleGroupPremiumButtons[id] != nil { + continue + } + let view = viewAndFrame.0 + self.scrollView.addSubview(view) + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + } else if let previousVisibleBoundingRect = previousVisibleBoundingRect { + var updatedVisibleBoundingRect: CGRect? + + for (_, layer) in self.visibleItemLayers { + let frame = layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect { + updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue) + } else { + updatedVisibleBoundingRect = frame + } + } + for (_, view) in self.visibleItemPlaceholderViews { + let frame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect { + updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue) + } else { + updatedVisibleBoundingRect = frame + } + } + for (_, layer) in self.visibleGroupHeaders { + if !self.scrollView.bounds.intersects(layer.frame) { + continue + } + let frame = layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect { + updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue) + } else { + updatedVisibleBoundingRect = frame + } + } + for (_, view) in self.visibleGroupPremiumButtons { + if let view = view.view { + if !self.scrollView.bounds.intersects(view.frame) { + continue + } + + let frame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect { + updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue) + } else { + updatedVisibleBoundingRect = frame + } + } + } + + if let updatedVisibleBoundingRect = updatedVisibleBoundingRect { + var commonItemOffset = updatedVisibleBoundingRect.height * offsetDirectionSign + + if previousVisibleBoundingRect.intersects(updatedVisibleBoundingRect) { + if offsetDirectionSign < 0.0 { + commonItemOffset = previousVisibleBoundingRect.minY - updatedVisibleBoundingRect.maxY + } else { + commonItemOffset = previousVisibleBoundingRect.maxY - updatedVisibleBoundingRect.minY + } + } + + for (_, layer) in self.visibleItemLayers { + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, layerAndFrame) in previousVisibleLayers { + if self.visibleItemLayers[id] != nil { + continue + } + let layer = layerAndFrame.0 + layer.frame = layerAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + self.scrollView.layer.addSublayer(layer) + layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in + layer?.removeFromSuperlayer() + }) + } + + for (_, view) in self.visibleItemPlaceholderViews { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, viewAndFrame) in previousVisiblePlaceholderViews { + if self.visibleItemPlaceholderViews[id] != nil { + continue + } + let view = viewAndFrame.0 + view.frame = viewAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + self.placeholdersContainerView.addSubview(view) + view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + + for (_, layer) in self.visibleGroupHeaders { + if !self.scrollView.bounds.intersects(layer.frame) { + continue + } + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, layerAndFrame) in previousVisibleGroupHeaders { + if self.visibleGroupHeaders[id] != nil { + continue + } + let layer = layerAndFrame.0 + layer.frame = layerAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + self.scrollView.layer.addSublayer(layer) + layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in + layer?.removeFromSuperlayer() + }) + } + + for (_, layer) in self.visibleGroupBorders { + if !self.scrollView.bounds.intersects(layer.frame) { + continue + } + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, layerAndFrame) in previousVisibleGroupBorders { + if self.visibleGroupBorders[id] != nil { + continue + } + let layer = layerAndFrame.0 + layer.frame = layerAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + self.scrollView.layer.addSublayer(layer) + layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in + layer?.removeFromSuperlayer() + }) + } + + for (_, view) in self.visibleGroupPremiumButtons { + if let view = view.view { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + } + for (id, viewAndFrame) in previousVisibleGroupPremiumButtons { + if self.visibleGroupPremiumButtons[id] != nil { + continue + } + let view = viewAndFrame.0 + view.frame = viewAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + self.scrollView.addSubview(view) + view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + } + } } } } @@ -1033,7 +1505,11 @@ public final class EmojiPagerContentComponent: Component { let locationInScrollView = recognizer.location(in: self.scrollView) outer: for (id, groupHeader) in self.visibleGroupHeaders { if groupHeader.frame.insetBy(dx: -10.0, dy: -6.0).contains(locationInScrollView) { - let _ = id + let groupHeaderPoint = self.scrollView.layer.convert(locationInScrollView, to: groupHeader) + if let clearIconLayer = groupHeader.clearIconLayer, clearIconLayer.frame.insetBy(dx: -4.0, dy: -4.0).contains(groupHeaderPoint) { + component.inputInteraction.clearGroup(id) + } + /*for group in component.itemGroups { if group.groupId == id { if group.isPremium && !self.expandedPremiumGroups.contains(id) { @@ -1180,11 +1656,12 @@ public final class EmojiPagerContentComponent: Component { } private func updateVisibleItems(transition: Transition, attemptSynchronousLoads: Bool) { - guard let component = self.component, let theme = self.theme, let itemLayout = self.itemLayout else { + guard let component = self.component, let pagerEnvironment = self.pagerEnvironment, let theme = self.theme, let itemLayout = self.itemLayout else { return } var topVisibleGroupId: AnyHashable? + var topVisibleSubgroupId: AnyHashable? var validIds = Set() var validGroupHeaderIds = Set() @@ -1192,14 +1669,16 @@ public final class EmojiPagerContentComponent: Component { var validGroupPremiumButtonIds = Set() let effectiveVisibleBounds = CGRect(origin: self.scrollView.bounds.origin, size: self.effectiveVisibleSize) - let topVisibleDetectionBounds = effectiveVisibleBounds.offsetBy(dx: 0.0, dy: 41.0) + let topVisibleDetectionBounds = effectiveVisibleBounds.offsetBy(dx: 0.0, dy: pagerEnvironment.containerInsets.top) for groupItems in itemLayout.visibleItems(for: effectiveVisibleBounds) { let itemGroup = component.itemGroups[groupItems.groupIndex] let itemGroupLayout = itemLayout.itemGroupLayouts[groupItems.groupIndex] + var assignTopVisibleSubgroupId = false if topVisibleGroupId == nil && itemGroupLayout.frame.intersects(topVisibleDetectionBounds) { topVisibleGroupId = groupItems.supergroupId + assignTopVisibleSubgroupId = true } var headerSize: CGSize? @@ -1216,19 +1695,21 @@ public final class EmojiPagerContentComponent: Component { self.visibleGroupHeaders[itemGroup.groupId] = groupHeaderLayer self.scrollView.layer.addSublayer(groupHeaderLayer) } - let (groupHeaderSize, groupHeaderHorizontalOffset) = groupHeaderLayer.update(theme: theme, title: title, isPremium: itemGroup.isPremium, constrainedWidth: itemLayout.contentSize.width - itemLayout.containerInsets.left - itemLayout.containerInsets.right - 32.0) + let groupHeaderSize = groupHeaderLayer.update(theme: theme, title: title, isPremiumLocked: itemGroup.isPremiumLocked, hasClear: itemGroup.hasClear, constrainedWidth: itemLayout.contentSize.width - itemLayout.containerInsets.left - itemLayout.containerInsets.right) if groupHeaderLayer.bounds.size != groupHeaderSize { headerSizeUpdated = true } - let groupHeaderFrame = CGRect(origin: CGPoint(x: groupHeaderHorizontalOffset + floor((itemLayout.contentSize.width - groupHeaderSize.width - groupHeaderHorizontalOffset) / 2.0), y: itemGroupLayout.frame.minY + 1.0), size: groupHeaderSize) + let groupHeaderFrame = CGRect(origin: CGPoint(x: floor((itemLayout.contentSize.width - groupHeaderSize.width) / 2.0), y: itemGroupLayout.frame.minY + 1.0), size: groupHeaderSize) groupHeaderLayer.bounds = CGRect(origin: CGPoint(), size: groupHeaderFrame.size) groupHeaderTransition.setPosition(layer: groupHeaderLayer, position: CGPoint(x: groupHeaderFrame.midX, y: groupHeaderFrame.midY)) - headerSize = CGSize(width: groupHeaderSize.width + groupHeaderHorizontalOffset, height: groupHeaderSize.height) + headerSize = CGSize(width: groupHeaderSize.width, height: groupHeaderSize.height) } - if itemGroup.isPremium { + let groupBorderRadius: CGFloat = 16.0 + + if itemGroup.isPremiumLocked { validGroupBorderIds.insert(itemGroup.groupId) let groupBorderLayer: GroupBorderLayer var groupBorderTransition = transition @@ -1252,8 +1733,6 @@ public final class EmojiPagerContentComponent: Component { let groupBorderFrame = CGRect(origin: CGPoint(x: groupBorderHorizontalInset, y: itemGroupLayout.frame.minY + groupBorderVerticalTopOffset), size: CGSize(width: itemLayout.width - groupBorderHorizontalInset * 2.0, height: itemGroupLayout.frame.size.height - groupBorderVerticalTopOffset + groupBorderVerticalInset)) - let radius: CGFloat = 16.0 - if groupBorderLayer.bounds.size != groupBorderFrame.size || headerSizeUpdated { let headerWidth: CGFloat if let headerSize = headerSize { @@ -1262,6 +1741,7 @@ public final class EmojiPagerContentComponent: Component { headerWidth = 0.0 } let path = CGMutablePath() + let radius = groupBorderRadius path.move(to: CGPoint(x: floor((groupBorderFrame.width - headerWidth) / 2.0), y: 0.0)) path.addLine(to: CGPoint(x: radius, y: 0.0)) path.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: 0.0, y: radius), radius: radius) @@ -1287,8 +1767,12 @@ public final class EmojiPagerContentComponent: Component { groupBorderTransition.setShapeLayerLineDashPattern(layer: groupBorderLayer, pattern: [(5.0 + dashSpace) as NSNumber, (7.0 + dashSpace) as NSNumber]) } groupBorderTransition.setFrame(layer: groupBorderLayer, frame: groupBorderFrame) + } + + if itemGroup.isPremiumLocked || itemGroup.isFeatured { + let groupPremiumButtonMeasuringFrame = CGRect(origin: CGPoint(x: itemLayout.containerInsets.left, y: itemGroupLayout.frame.maxY - 50.0 + 1.0), size: CGSize(width: 100.0, height: 50.0)) - if itemGroup.isPremium { + if effectiveVisibleBounds.intersects(groupPremiumButtonMeasuringFrame) { validGroupPremiumButtonIds.insert(itemGroup.groupId) let groupPremiumButton: ComponentView @@ -1301,34 +1785,59 @@ public final class EmojiPagerContentComponent: Component { self.visibleGroupPremiumButtons[itemGroup.groupId] = groupPremiumButton } + let groupId = itemGroup.groupId + let isPremiumLocked = itemGroup.isPremiumLocked + //TODO:localize + let title: String + let backgroundColor: UIColor + let backgroundColors: [UIColor] + let foregroundColor: UIColor + let animationName: String? + let gloss: Bool + if itemGroup.isPremiumLocked { + title = "Unlock \(itemGroup.title ?? "Emoji")" + backgroundColors = [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ] + backgroundColor = backgroundColors[0] + foregroundColor = .white + animationName = "premium_unlock" + gloss = true + } else { + title = "Add \(itemGroup.title ?? "Emoji")" + backgroundColors = [] + backgroundColor = theme.list.itemCheckColors.fillColor + foregroundColor = theme.list.itemCheckColors.foregroundColor + animationName = nil + gloss = false + } + let groupPremiumButtonSize = groupPremiumButton.update( transition: groupPremiumButtonTransition, component: AnyComponent(SolidRoundedButtonComponent( - title: "Unlock \(itemGroup.title ?? "Emoji")", + title: title, theme: SolidRoundedButtonComponent.Theme( - backgroundColor: .black, - backgroundColors: [ - UIColor(rgb: 0x0077ff), - UIColor(rgb: 0x6b93ff), - UIColor(rgb: 0x8878ff), - UIColor(rgb: 0xe46ace) - ], - foregroundColor: .white + backgroundColor: backgroundColor, + backgroundColors: backgroundColors, + foregroundColor: foregroundColor ), font: .bold, fontSize: 17.0, height: 50.0, - cornerRadius: radius, - gloss: true, - animationName: "premium_unlock", + cornerRadius: groupBorderRadius, + gloss: gloss, + animationName: animationName, iconPosition: .right, iconSpacing: 4.0, action: { [weak self] in guard let strongSelf = self, let component = strongSelf.component else { return } - component.inputInteraction.openPremiumSection() + component.inputInteraction.addGroupAction(groupId, isPremiumLocked) } )), environment: {}, @@ -1351,114 +1860,128 @@ public final class EmojiPagerContentComponent: Component { } } - for index in groupItems.groupItems.lowerBound ..< groupItems.groupItems.upperBound { - let item = itemGroup.items[index] - let itemId = ItemLayer.Key(groupId: itemGroup.groupId, fileId: item.file?.fileId, staticEmoji: item.staticEmoji) - validIds.insert(itemId) - - let itemDimensions: CGSize - if let file = item.file { - itemDimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) - } else { - itemDimensions = CGSize(width: 512.0, height: 512.0) - } - let itemNativeFitSize = itemDimensions.fitted(CGSize(width: itemLayout.nativeItemSize, height: itemLayout.nativeItemSize)) - let itemVisibleFitSize = itemDimensions.fitted(CGSize(width: itemLayout.visibleItemSize, height: itemLayout.visibleItemSize)) - - var updateItemLayerPlaceholder = false - var itemTransition = transition - let itemLayer: ItemLayer - if let current = self.visibleItemLayers[itemId] { - itemLayer = current - } else { - updateItemLayerPlaceholder = true - itemTransition = .immediate + if let groupItemRange = groupItems.groupItems { + for index in groupItemRange.lowerBound ..< groupItemRange.upperBound { + let item = itemGroup.items[index] - itemLayer = ItemLayer( - item: item, - context: component.context, - attemptSynchronousLoad: attemptSynchronousLoads, - file: item.file, - staticEmoji: item.staticEmoji, - cache: component.animationCache, - renderer: component.animationRenderer, - placeholderColor: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1), - blurredBadgeColor: theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5), - displayPremiumBadgeIfAvailable: itemGroup.displayPremiumBadges, - pointSize: itemNativeFitSize, - onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in - guard let strongSelf = self else { - return - } - if displayPlaceholder, let file = item.file { - if let itemLayer = strongSelf.visibleItemLayers[itemId] { - let placeholderView: ItemPlaceholderView - if let current = strongSelf.visibleItemPlaceholderViews[itemId] { - placeholderView = current - } else { - placeholderView = ItemPlaceholderView( - context: component.context, - file: file, - shimmerView: strongSelf.shimmerHostView, - color: nil, - size: itemNativeFitSize - ) - strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView - strongSelf.placeholdersContainerView.addSubview(placeholderView) - } - placeholderView.frame = itemLayer.frame - placeholderView.update(size: placeholderView.bounds.size) - - strongSelf.updateShimmerIfNeeded() + if assignTopVisibleSubgroupId { + if let subgroupId = item.subgroupId { + topVisibleSubgroupId = AnyHashable(subgroupId) + } + } + + let itemId = ItemLayer.Key(groupId: itemGroup.groupId, fileId: item.file?.fileId, staticEmoji: item.staticEmoji) + validIds.insert(itemId) + + let itemDimensions: CGSize + if let file = item.file { + itemDimensions = file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) + } else { + itemDimensions = CGSize(width: 512.0, height: 512.0) + } + let itemNativeFitSize = itemDimensions.fitted(CGSize(width: itemLayout.nativeItemSize, height: itemLayout.nativeItemSize)) + let itemVisibleFitSize = itemDimensions.fitted(CGSize(width: itemLayout.visibleItemSize, height: itemLayout.visibleItemSize)) + + var updateItemLayerPlaceholder = false + var itemTransition = transition + let itemLayer: ItemLayer + if let current = self.visibleItemLayers[itemId] { + itemLayer = current + } else { + updateItemLayerPlaceholder = true + itemTransition = .immediate + + itemLayer = ItemLayer( + item: item, + context: component.context, + attemptSynchronousLoad: attemptSynchronousLoads, + file: item.file, + staticEmoji: item.staticEmoji, + cache: component.animationCache, + renderer: component.animationRenderer, + placeholderColor: theme.chat.inputPanel.primaryTextColor.withMultipliedAlpha(0.1), + blurredBadgeColor: theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(0.5), + pointSize: itemNativeFitSize, + onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in + guard let strongSelf = self else { + return } - } else { - if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] { - strongSelf.visibleItemPlaceholderViews.removeValue(forKey: itemId) - - if duration > 0.0 { - placeholderView.layer.opacity = 0.0 - placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak self, weak placeholderView] _ in - guard let strongSelf = self else { - return - } - placeholderView?.removeFromSuperview() - strongSelf.updateShimmerIfNeeded() - }) - } else { - placeholderView.removeFromSuperview() + if displayPlaceholder, let file = item.file { + if let itemLayer = strongSelf.visibleItemLayers[itemId] { + let placeholderView: ItemPlaceholderView + if let current = strongSelf.visibleItemPlaceholderViews[itemId] { + placeholderView = current + } else { + placeholderView = ItemPlaceholderView( + context: component.context, + file: file, + shimmerView: strongSelf.shimmerHostView, + color: nil, + size: itemNativeFitSize + ) + strongSelf.visibleItemPlaceholderViews[itemId] = placeholderView + strongSelf.placeholdersContainerView.addSubview(placeholderView) + } + placeholderView.frame = itemLayer.frame + placeholderView.update(size: placeholderView.bounds.size) + strongSelf.updateShimmerIfNeeded() } + } else { + if let placeholderView = strongSelf.visibleItemPlaceholderViews[itemId] { + strongSelf.visibleItemPlaceholderViews.removeValue(forKey: itemId) + + if duration > 0.0 { + placeholderView.layer.opacity = 0.0 + placeholderView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { [weak self, weak placeholderView] _ in + guard let strongSelf = self else { + return + } + placeholderView?.removeFromSuperview() + strongSelf.updateShimmerIfNeeded() + }) + } else { + placeholderView.removeFromSuperview() + strongSelf.updateShimmerIfNeeded() + } + } } } + ) + self.scrollView.layer.addSublayer(itemLayer) + self.visibleItemLayers[itemId] = itemLayer + } + + var itemFrame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: index) + + itemFrame.origin.x += floor((itemFrame.width - itemVisibleFitSize.width) / 2.0) + itemFrame.origin.y += floor((itemFrame.height - itemVisibleFitSize.height) / 2.0) + itemFrame.size = itemVisibleFitSize + + let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY) + let itemBounds = CGRect(origin: CGPoint(), size: itemFrame.size) + itemTransition.setPosition(layer: itemLayer, position: itemPosition) + itemTransition.setBounds(layer: itemLayer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + + var badge: ItemLayer.Badge? + if itemGroup.displayPremiumBadges, let file = item.file, file.isPremiumSticker { + badge = .premium + } + itemLayer.update(transition: transition, size: itemFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: theme.list.plainBackgroundColor) + + if let placeholderView = self.visibleItemPlaceholderViews[itemId] { + if placeholderView.layer.position != itemPosition || placeholderView.layer.bounds != itemBounds { + itemTransition.setFrame(view: placeholderView, frame: itemFrame) + placeholderView.update(size: itemFrame.size) + } + } else if updateItemLayerPlaceholder { + if itemLayer.displayPlaceholder { + itemLayer.onUpdateDisplayPlaceholder(true, 0.0) } - ) - self.scrollView.layer.addSublayer(itemLayer) - self.visibleItemLayers[itemId] = itemLayer - } - - var itemFrame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: index) - - itemFrame.origin.x += floor((itemFrame.width - itemVisibleFitSize.width) / 2.0) - itemFrame.origin.y += floor((itemFrame.height - itemVisibleFitSize.height) / 2.0) - itemFrame.size = itemVisibleFitSize - - let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY) - let itemBounds = CGRect(origin: CGPoint(), size: itemFrame.size) - itemTransition.setPosition(layer: itemLayer, position: itemPosition) - itemTransition.setBounds(layer: itemLayer, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) - - if let placeholderView = self.visibleItemPlaceholderViews[itemId] { - if placeholderView.layer.position != itemPosition || placeholderView.layer.bounds != itemBounds { - itemTransition.setFrame(view: placeholderView, frame: itemFrame) - placeholderView.update(size: itemFrame.size) - } - } else if updateItemLayerPlaceholder { - if itemLayer.displayPlaceholder { - itemLayer.onUpdateDisplayPlaceholder(true, 0.0) } + + itemLayer.isVisibleForAnimations = true } - - itemLayer.isVisibleForAnimations = true } } @@ -1517,7 +2040,7 @@ public final class EmojiPagerContentComponent: Component { } if let topVisibleGroupId = topVisibleGroupId { - self.activeItemUpdated?.invoke((topVisibleGroupId, .immediate)) + self.activeItemUpdated?.invoke((topVisibleGroupId, topVisibleSubgroupId, .immediate)) } } @@ -1530,6 +2053,8 @@ public final class EmojiPagerContentComponent: Component { } func update(component: EmojiPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let previousComponent = self.component + self.component = component self.state = state @@ -1549,13 +2074,37 @@ public final class EmojiPagerContentComponent: Component { let shimmerForegroundColor = keyboardChildEnvironment.theme.list.itemBlocksBackgroundColor.withMultipliedAlpha(0.15) self.standaloneShimmerEffect.update(background: shimmerBackgroundColor, foreground: shimmerForegroundColor) + var anchorItem: (key: ItemLayer.Key, frame: CGRect)? + if let previousComponent = previousComponent, previousComponent.itemGroups != component.itemGroups { + let effectiveVisibleBounds = CGRect(origin: self.scrollView.bounds.origin, size: self.effectiveVisibleSize) + let topVisibleDetectionBounds = effectiveVisibleBounds.offsetBy(dx: 0.0, dy: pagerEnvironment.containerInsets.top) + for (key, itemLayer) in self.visibleItemLayers { + if !topVisibleDetectionBounds.intersects(itemLayer.frame) { + continue + } + if let anchorItemValue = anchorItem { + if itemLayer.frame.minY < anchorItemValue.frame.minY { + anchorItem = (key, itemLayer.frame) + } else if itemLayer.frame.minY == anchorItemValue.frame.minY && itemLayer.frame.minX < anchorItemValue.frame.minX { + anchorItem = (key, itemLayer.frame) + } + } else { + anchorItem = (key, itemLayer.frame) + } + } + if let anchorItemValue = anchorItem { + anchorItem = (anchorItemValue.key, self.scrollView.convert(anchorItemValue.frame, to: self)) + } + } + var itemGroups: [ItemGroupDescription] = [] for itemGroup in component.itemGroups { itemGroups.append(ItemGroupDescription( supergroupId: itemGroup.supergroupId, groupId: itemGroup.groupId, hasTitle: itemGroup.title != nil, - isPremium: itemGroup.isPremium, + isPremiumLocked: itemGroup.isPremiumLocked, + isFeatured: itemGroup.isFeatured, itemCount: itemGroup.items.count )) } @@ -1602,6 +2151,40 @@ public final class EmojiPagerContentComponent: Component { self.scrollView.scrollIndicatorInsets = pagerEnvironment.containerInsets } self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: scrollView.isDragging || scrollView.isDecelerating) + + if let anchorItem = anchorItem { + outer: for i in 0 ..< component.itemGroups.count { + if component.itemGroups[i].groupId != anchorItem.key.groupId { + continue + } + for j in 0 ..< component.itemGroups[i].items.count { + let itemKey: ItemLayer.Key + if let file = component.itemGroups[i].items[j].file { + itemKey = ItemLayer.Key(groupId: component.itemGroups[i].groupId, fileId: file.fileId, staticEmoji: nil) + } else if let staticEmoji = component.itemGroups[i].items[j].staticEmoji { + itemKey = ItemLayer.Key(groupId: component.itemGroups[i].groupId, fileId: nil, staticEmoji: staticEmoji) + } else { + continue + } + + if itemKey == anchorItem.key { + let itemFrame = itemLayout.frame(groupIndex: i, itemIndex: j) + + var contentOffsetY = itemFrame.minY - anchorItem.frame.minY + if contentOffsetY > self.scrollView.contentSize.height - self.scrollView.bounds.height { + contentOffsetY = self.scrollView.contentSize.height - self.scrollView.bounds.height + } + if contentOffsetY < 0.0 { + contentOffsetY = 0.0 + } + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: contentOffsetY), animated: false) + + break outer + } + } + } + } + self.ignoreScrolling = false self.updateVisibleItems(transition: itemTransition, attemptSynchronousLoads: !(scrollView.isDragging || scrollView.isDecelerating)) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 75011510bb..b86a6fdaad 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -13,11 +13,11 @@ import SwiftSignalKit public final class EntityKeyboardChildEnvironment: Equatable { public let theme: PresentationTheme - public let getContentActiveItemUpdated: (AnyHashable) -> ActionSlot<(AnyHashable, Transition)>? + public let getContentActiveItemUpdated: (AnyHashable) -> ActionSlot<(AnyHashable, AnyHashable?, Transition)>? public init( theme: PresentationTheme, - getContentActiveItemUpdated: @escaping (AnyHashable) -> ActionSlot<(AnyHashable, Transition)>? + getContentActiveItemUpdated: @escaping (AnyHashable) -> ActionSlot<(AnyHashable, AnyHashable?, Transition)>? ) { self.theme = theme self.getContentActiveItemUpdated = getContentActiveItemUpdated @@ -200,8 +200,8 @@ public final class EntityKeyboardComponent: Component { var contentAccessoryLeftButtons: [AnyComponentWithIdentity] = [] var contentAccessoryRightButtons: [AnyComponentWithIdentity] = [] - let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>() - let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>() + let gifsContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>() + let stickersContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>() if transition.userData(MarkInputCollapsed.self) != nil { self.searchComponent = nil @@ -242,6 +242,8 @@ public final class EntityKeyboardComponent: Component { content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( context: component.emojiContent.context, file: emoji.file, + isFeatured: false, + isPremiumLocked: false, animationCache: component.emojiContent.animationCache, animationRenderer: component.emojiContent.animationRenderer, theme: component.theme, @@ -293,8 +295,9 @@ public final class EntityKeyboardComponent: Component { let iconMapping: [String: String] = [ "saved": "Chat/Input/Media/SavedStickersTabIcon", "recent": "Chat/Input/Media/RecentTabIcon", - "premium": "Chat/Input/Media/PremiumIcon" + "premium": "Peer Info/PremiumIcon" ] + //TODO:localize let titleMapping: [String: String] = [ "saved": "Saved", "recent": "Recent", @@ -323,6 +326,8 @@ public final class EntityKeyboardComponent: Component { content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( context: stickerContent.context, file: file, + isFeatured: itemGroup.isFeatured, + isPremiumLocked: itemGroup.isPremiumLocked, animationCache: stickerContent.animationCache, animationRenderer: stickerContent.animationRenderer, theme: component.theme, @@ -375,18 +380,42 @@ public final class EntityKeyboardComponent: Component { ).minSize(CGSize(width: 38.0, height: 38.0))))) } - let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, Transition)>() + let emojiContentItemIdUpdated = ActionSlot<(AnyHashable, AnyHashable?, Transition)>() contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(component.emojiContent))) var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] for itemGroup in component.emojiContent.itemGroups { if !itemGroup.items.isEmpty { if let id = itemGroup.groupId.base as? String { - if id == "static" { + if id == "recent" { + let iconMapping: [String: String] = [ + "recent": "Chat/Input/Media/RecentTabIcon", + ] + //TODO:localize + let titleMapping: [String: String] = [ + "recent": "Recent", + ] + if let iconName = iconMapping[id], let title = titleMapping[id] { + topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: false, + content: AnyComponent(EntityKeyboardIconTopPanelComponent( + imageName: iconName, + theme: component.theme, + title: title, + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) + } + )) + )) + } + } else if id == "static" { + //TODO:localize topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( id: itemGroup.supergroupId, isReorderable: false, content: AnyComponent(EntityKeyboardStaticStickersPanelComponent( theme: component.theme, + title: "Emoji", pressed: { [weak self] subgroupId in guard let strongSelf = self else { return @@ -404,6 +433,8 @@ public final class EntityKeyboardComponent: Component { content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( context: component.emojiContent.context, file: file, + isFeatured: itemGroup.isFeatured, + isPremiumLocked: itemGroup.isPremiumLocked, animationCache: component.emojiContent.animationCache, animationRenderer: component.emojiContent.animationRenderer, theme: component.theme, @@ -651,9 +682,17 @@ public final class EntityKeyboardComponent: Component { } private func scrollToItemGroup(contentId: String, groupId: AnyHashable, subgroupId: Int32?) { - if let pagerView = self.pagerView.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: contentId)) as? EmojiPagerContentComponent.View { - pagerView.scrollToItemGroup(id: groupId, subgroupId: subgroupId) + guard let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent.View else { + return } + guard let pagerContentView = self.pagerView.findTaggedView(tag: EmojiPagerContentComponent.Tag(id: contentId)) as? EmojiPagerContentComponent.View else { + return + } + if let topPanelView = pagerView.topPanelComponentView as? EntityKeyboardTopContainerPanelComponent.View { + topPanelView.internalUpdatePanelsAreCollapsed() + } + pagerContentView.scrollToItemGroup(id: groupId, subgroupId: subgroupId) + pagerView.collapseTopPanel() } private func reorderPacks(category: ReorderCategory, items: [EntityKeyboardTopPanelComponent.Item]) { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift index 54bacc37aa..7057727731 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift @@ -91,8 +91,6 @@ final class EntityKeyboardTopContainerPanelComponent: Component { let intrinsicHeight: CGFloat = 41.0 let height = intrinsicHeight - let isExpanded = availableSize.height > 41.0 - let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value var transitionOffsetFraction: CGFloat = 0.0 @@ -142,10 +140,6 @@ final class EntityKeyboardTopContainerPanelComponent: Component { self.addSubview(panelView.view) } - if !isExpanded { - panelView.isExpanded = false - } - let panelId = panel.id let _ = panelView.view.update( transition: panelTransition, @@ -261,6 +255,12 @@ final class EntityKeyboardTopContainerPanelComponent: Component { self.panelEnvironment?.isExpandedUpdated(hasExpanded, transition) } + public func internalUpdatePanelsAreCollapsed() { + for (_, panelView) in self.panelViews { + panelView.isExpanded = false + } + } + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.alpha.isZero { return nil diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 7e2fe8382a..ab8c7057d8 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -18,6 +18,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { let context: AccountContext let file: TelegramMediaFile + let isFeatured: Bool + let isPremiumLocked: Bool let animationCache: AnimationCache let animationRenderer: MultiAnimationRenderer let theme: PresentationTheme @@ -27,6 +29,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { init( context: AccountContext, file: TelegramMediaFile, + isFeatured: Bool, + isPremiumLocked: Bool, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, theme: PresentationTheme, @@ -35,6 +39,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { ) { self.context = context self.file = file + self.isFeatured = isFeatured + self.isPremiumLocked = isPremiumLocked self.animationCache = animationCache self.animationRenderer = animationRenderer self.theme = theme @@ -49,6 +55,12 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { if lhs.file.fileId != rhs.file.fileId { return false } + if lhs.isFeatured != rhs.isFeatured { + return false + } + if lhs.isPremiumLocked != rhs.isPremiumLocked { + return false + } if lhs.animationCache !== rhs.animationCache { return false } @@ -107,7 +119,6 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { renderer: component.animationRenderer, placeholderColor: .lightGray, blurredBadgeColor: .clear, - displayPremiumBadgeIfAvailable: false, pointSize: CGSize(width: 44.0, height: 44.0), onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder, duration in guard let strongSelf = self else { @@ -130,6 +141,15 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { if let itemLayer = self.itemLayer { transition.setPosition(layer: itemLayer, position: CGPoint(x: iconFrame.midX, y: iconFrame.midY)) transition.setBounds(layer: itemLayer, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + + var badge: EmojiPagerContentComponent.View.ItemLayer.Badge? + if component.isPremiumLocked { + badge = .locked + } else if component.isFeatured { + badge = .featured + } + itemLayer.update(transition: transition, size: iconFrame.size, badge: badge, blurredBadgeColor: UIColor(white: 0.0, alpha: 0.1), blurredBadgeBackgroundColor: component.theme.list.plainBackgroundColor) + itemLayer.isVisibleForAnimations = true } @@ -144,7 +164,8 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { let titleSize = titleView.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)) + text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)), + insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) )), environment: {}, containerSize: CGSize(width: 62.0, height: 100.0) @@ -154,7 +175,7 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { view.alpha = 0.0 self.addSubview(view) } - view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height), size: titleSize) + view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height - 1.0), size: titleSize) transition.setAlpha(view: view, alpha: 1.0) } } else if let titleView = self.titleView { @@ -274,12 +295,35 @@ final class EntityKeyboardIconTopPanelComponent: Component { if self.component?.imageName != component.imageName { self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: component.imageName), color: component.theme.chat.inputMediaPanel.panelIconColor) + + if component.imageName.hasSuffix("PremiumIcon") { + self.iconView.image = generateImage(CGSize(width: 44.0, height: 42.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let image = UIImage(bundleImageName: "Peer Info/PremiumIcon") { + if let cgImage = image.cgImage { + context.clip(to: CGRect(origin: .zero, size: size), mask: cgImage) + } + + let colorsArray: [CGColor] = [ + UIColor(rgb: 0x6B93FF).cgColor, + UIColor(rgb: 0x6B93FF).cgColor, + UIColor(rgb: 0x976FFF).cgColor, + UIColor(rgb: 0xE46ACE).cgColor, + UIColor(rgb: 0xE46ACE).cgColor + ] + var locations: [CGFloat] = [0.0, 0.35, 0.5, 0.65, 1.0] + let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: CGGradientDrawingOptions()) + } + }) + } } self.component = component let nativeIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 28.0, height: 28.0) - let boundingIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 38.0, height: 38.0) : CGSize(width: 24.0, height: 24.0) + let boundingIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 38.0, height: 38.0) : CGSize(width: 28.0, height: 28.0) let iconSize = (self.iconView.image?.size ?? nativeIconSize).aspectFitted(boundingIconSize) let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) / 2.0), y: floor((nativeIconSize.height - iconSize.height) / 2.0)), size: iconSize) @@ -297,7 +341,8 @@ final class EntityKeyboardIconTopPanelComponent: Component { let titleSize = titleView.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)) + text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)), + insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) )), environment: {}, containerSize: CGSize(width: 62.0, height: 100.0) @@ -307,7 +352,7 @@ final class EntityKeyboardIconTopPanelComponent: Component { view.alpha = 0.0 self.addSubview(view) } - view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height), size: titleSize) + view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height - 1.0), size: titleSize) transition.setAlpha(view: view, alpha: 1.0) } } else if let titleView = self.titleView { @@ -336,13 +381,16 @@ final class EntityKeyboardStaticStickersPanelComponent: Component { typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment let theme: PresentationTheme + let title: String let pressed: (EmojiPagerContentComponent.StaticEmojiSegment) -> Void init( theme: PresentationTheme, + title: String, pressed: @escaping (EmojiPagerContentComponent.StaticEmojiSegment) -> Void ) { self.theme = theme + self.title = title self.pressed = pressed } @@ -350,19 +398,29 @@ final class EntityKeyboardStaticStickersPanelComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.title != rhs.title { + return false + } return true } final class View: UIView, UIScrollViewDelegate { + private let scrollViewContainer: UIView private let scrollView: UIScrollView private var visibleItemViews: [EmojiPagerContentComponent.StaticEmojiSegment: ComponentView] = [:] + private var titleView: ComponentView? + private var component: EntityKeyboardStaticStickersPanelComponent? + private var itemEnvironment: EntityKeyboardTopPanelItemEnvironment? private var ignoreScrolling: Bool = false override init(frame: CGRect) { + self.scrollViewContainer = UIView() + self.scrollViewContainer.clipsToBounds = true + self.scrollView = UIScrollView() super.init(frame: frame) @@ -380,9 +438,9 @@ final class EntityKeyboardStaticStickersPanelComponent: Component { self.scrollView.showsHorizontalScrollIndicator = false self.scrollView.alwaysBounceHorizontal = false self.scrollView.delegate = self - self.addSubview(self.scrollView) - self.clipsToBounds = true + self.scrollViewContainer.addSubview(self.scrollView) + self.addSubview(self.scrollViewContainer) self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) } @@ -411,31 +469,36 @@ final class EntityKeyboardStaticStickersPanelComponent: Component { } private func updateVisibleItems(transition: Transition, animateAppearingItems: Bool) { - guard let component = self.component else { + guard let component = self.component, let itemEnvironment = self.itemEnvironment else { return } - let _ = component - var validItemIds = Set() let visibleBounds = self.scrollView.bounds - let componentHeight: CGFloat = 32.0 + let componentHeight: CGFloat = self.scrollView.contentSize.height + + let isExpanded = componentHeight > 32.0 let items = EmojiPagerContentComponent.StaticEmojiSegment.allCases - let itemSize: CGFloat = 28.0 + let itemSize: CGFloat = isExpanded ? 42.0 : 32.0 let itemSpacing: CGFloat = 4.0 - let sideInset: CGFloat = 2.0 + let sideInset: CGFloat = isExpanded ? 5.0 : 2.0 + let itemOffset: CGFloat = isExpanded ? -8.0 : 0.0 for i in 0 ..< items.count { - let itemFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (itemSize + itemSpacing), y: floor(componentHeight - itemSize) / 2.0), size: CGSize(width: itemSize, height: itemSize)) + let itemFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (itemSize + itemSpacing), y: floor(componentHeight - itemSize) / 2.0 + itemOffset), size: CGSize(width: itemSize, height: itemSize)) if visibleBounds.intersects(itemFrame) { let item = items[i] validItemIds.insert(item) + var animateItem = false + var itemTransition = transition let itemView: ComponentView if let current = self.visibleItemViews[item] { itemView = current } else { + animateItem = animateAppearingItems + itemTransition = .immediate itemView = ComponentView() self.visibleItemViews[item] = itemView } @@ -460,14 +523,21 @@ final class EntityKeyboardStaticStickersPanelComponent: Component { animationName = "emojicat_flags" } + let color: UIColor + if itemEnvironment.highlightedSubgroupId == AnyHashable(items[i].rawValue) { + color = component.theme.chat.inputMediaPanel.panelIconColor.mixedWith(component.theme.chat.inputPanel.primaryTextColor, alpha: 0.35) + } else { + color = component.theme.chat.inputMediaPanel.panelIconColor + } + let _ = itemView.update( - transition: .immediate, + transition: itemTransition, component: AnyComponent(LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: animationName, - colors: ["__allcolors__": component.theme.chat.inputMediaPanel.panelIconColor], - mode: animateAppearingItems ? .animating(loop: false) : .still(position: .end) + mode: animateItem ? .animating(loop: false) : .still(position: .end) ), + colors: ["__allcolors__": color], size: CGSize(width: itemSize, height: itemSize) )), environment: {}, @@ -477,7 +547,7 @@ final class EntityKeyboardStaticStickersPanelComponent: Component { if view.superview == nil { self.scrollView.addSubview(view) } - view.frame = itemFrame + itemTransition.setFrame(view: view, frame: itemFrame) } } } @@ -495,26 +565,80 @@ final class EntityKeyboardStaticStickersPanelComponent: Component { } func update(component: EntityKeyboardStaticStickersPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - self.layer.cornerRadius = availableSize.height / 2.0 + transition.setFrame(view: self.scrollViewContainer, frame: CGRect(origin: CGPoint(), size: availableSize)) + transition.setCornerRadius(layer: self.scrollViewContainer.layer, cornerRadius: min(availableSize.width / 2.0, availableSize.height / 2.0)) let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value - self.component = component + var scrollToItem: AnyHashable? + if itemEnvironment.highlightedSubgroupId != self.itemEnvironment?.highlightedSubgroupId { + scrollToItem = itemEnvironment.highlightedSubgroupId + } - let itemSize: CGFloat = 28.0 + self.component = component + self.itemEnvironment = itemEnvironment + + let isExpanded = itemEnvironment.isExpanded + let itemSize: CGFloat = isExpanded ? 42.0 : 32.0 let itemSpacing: CGFloat = 4.0 - let sideInset: CGFloat = 2.0 + let sideInset: CGFloat = isExpanded ? 5.0 : 2.0 let itemCount = EmojiPagerContentComponent.StaticEmojiSegment.allCases.count self.ignoreScrolling = true - self.scrollView.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(availableSize.width, 150.0), height: availableSize.height)) + self.scrollView.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(availableSize.width, 160.0), height: availableSize.height)) self.scrollView.contentSize = CGSize(width: sideInset * 2.0 + itemSize * CGFloat(itemCount) + itemSpacing * CGFloat(itemCount - 1), height: availableSize.height) self.ignoreScrolling = false - self.updateVisibleItems(transition: .immediate, animateAppearingItems: false) + self.updateVisibleItems(transition: transition, animateAppearingItems: false) - if !itemEnvironment.isHighlighted && self.scrollView.contentOffset.x != 0.0 { + if (!itemEnvironment.isHighlighted || isExpanded) && self.scrollView.contentOffset.x != 0.0 { self.scrollView.setContentOffset(CGPoint(), animated: true) + scrollToItem = nil + } + + if let scrollToItem = scrollToItem { + let items = EmojiPagerContentComponent.StaticEmojiSegment.allCases + for i in 0 ..< items.count { + if AnyHashable(items[i].rawValue) == scrollToItem { + let itemFrame = CGRect(origin: CGPoint(x: sideInset + CGFloat(i) * (itemSize + itemSpacing), y: 0.0), size: CGSize(width: itemSize, height: itemSize)) + self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: -sideInset, dy: 0.0), animated: true) + break + } + } + } + + if itemEnvironment.isExpanded { + let titleView: ComponentView + if let current = self.titleView { + titleView = current + } else { + titleView = ComponentView() + self.titleView = titleView + } + let titleSize = titleView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)), + insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + )), + environment: {}, + containerSize: CGSize(width: 62.0, height: 100.0) + ) + if let view = titleView.view { + if view.superview == nil { + view.alpha = 0.0 + self.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height - 4.0), size: titleSize) + transition.setAlpha(view: view, alpha: 1.0) + } + } else if let titleView = self.titleView { + self.titleView = nil + if let view = titleView.view { + transition.setAlpha(view: view, alpha: 0.0, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } } return availableSize @@ -533,10 +657,12 @@ final class EntityKeyboardStaticStickersPanelComponent: Component { final class EntityKeyboardTopPanelItemEnvironment: Equatable { let isExpanded: Bool let isHighlighted: Bool + let highlightedSubgroupId: AnyHashable? - init(isExpanded: Bool, isHighlighted: Bool) { + init(isExpanded: Bool, isHighlighted: Bool, highlightedSubgroupId: AnyHashable?) { self.isExpanded = isExpanded self.isHighlighted = isHighlighted + self.highlightedSubgroupId = highlightedSubgroupId } static func ==(lhs: EntityKeyboardTopPanelItemEnvironment, rhs: EntityKeyboardTopPanelItemEnvironment) -> Bool { @@ -546,6 +672,9 @@ final class EntityKeyboardTopPanelItemEnvironment: Equatable { if lhs.isHighlighted != rhs.isHighlighted { return false } + if lhs.highlightedSubgroupId != rhs.highlightedSubgroupId { + return false + } return true } } @@ -617,6 +746,8 @@ private final class ReorderGestureRecognizer: UIGestureRecognizer { self.stopLongTapTimer() self.stopLongPressTimer() self.initialLocation = nil + + self.isActiveUpdated(false) } private func longTapTimerFired() { @@ -776,14 +907,14 @@ final class EntityKeyboardTopPanelComponent: Component { let theme: PresentationTheme let items: [Item] let defaultActiveItemId: AnyHashable? - let activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)> + let activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)> let reorderItems: ([Item]) -> Void init( theme: PresentationTheme, items: [Item], defaultActiveItemId: AnyHashable? = nil, - activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)>, + activeContentItemIdUpdated: ActionSlot<(AnyHashable, AnyHashable?, Transition)>, reorderItems: @escaping ([Item]) -> Void ) { self.theme = theme @@ -836,7 +967,7 @@ final class EntityKeyboardTopPanelComponent: Component { self.isExpanded = isExpanded self.itemSize = self.isExpanded ? CGSize(width: 54.0, height: 68.0) : CGSize(width: 32.0, height: 32.0) self.staticItemSize = self.itemSize - self.staticExpandedItemSize = self.isExpanded ? CGSize(width: 150.0, height: 68.0) : CGSize(width: 150.0, height: 32.0) + self.staticExpandedItemSize = self.isExpanded ? self.staticItemSize : CGSize(width: 160.0, height: 32.0) self.innerItemSize = self.isExpanded ? CGSize(width: 50.0, height: 62.0) : CGSize(width: 28.0, height: 28.0) var contentSize = CGSize(width: sideInset, height: height) @@ -887,11 +1018,19 @@ final class EntityKeyboardTopPanelComponent: Component { return self.items[index].frame } - func contentFrame(containerFrame: CGRect) -> CGRect { + func contentFrame(index: Int, containerFrame: CGRect) -> CGRect { + let outerFrame = self.items[index].frame + let innerFrame = self.items[index].innerFrame + + let sizeDifference = CGSize(width: outerFrame.width - innerFrame.width, height: outerFrame.height - innerFrame.height) + let offsetDifference = CGPoint(x: outerFrame.minX - innerFrame.minX, y: outerFrame.minY - innerFrame.minY) + var frame = containerFrame - frame.origin.x += floor((self.itemSize.width - self.innerItemSize.width)) / 2.0 - frame.origin.y += floor((self.itemSize.height - self.innerItemSize.height)) / 2.0 - frame.size = self.innerItemSize + frame.origin.x -= offsetDifference.x + frame.origin.y -= offsetDifference.y + frame.size.width -= sizeDifference.width + frame.size.height -= sizeDifference.height + return frame } @@ -933,12 +1072,17 @@ final class EntityKeyboardTopPanelComponent: Component { private var isReordering: Bool = false private var isDraggingOrReordering: Bool = false private var draggingStoppedTimer: SwiftSignalKit.Timer? + private var draggingFocusItemIndex: Int? + private var draggingEndOffset: CGFloat? private var isExpanded: Bool = false private var visibilityFraction: CGFloat = 1.0 private var activeContentItemId: AnyHashable? + private var activeSubcontentItemId: AnyHashable? + + private var reorderGestureRecognizer: ReorderGestureRecognizer? private var component: EntityKeyboardTopPanelComponent? weak var state: EmptyComponentState? @@ -980,7 +1124,7 @@ final class EntityKeyboardTopPanelComponent: Component { return strongSelf.scrollView.contentOffset.x > 0.0 } - self.addGestureRecognizer(ReorderGestureRecognizer( + let reorderGestureRecognizer = ReorderGestureRecognizer( shouldBegin: { [weak self] point in guard let strongSelf = self else { return (false, false, nil) @@ -1022,7 +1166,9 @@ final class EntityKeyboardTopPanelComponent: Component { } strongSelf.updateIsReordering(isActive) } - )) + ) + self.reorderGestureRecognizer = reorderGestureRecognizer + self.addGestureRecognizer(reorderGestureRecognizer) } required init?(coder: NSCoder) { @@ -1038,10 +1184,60 @@ final class EntityKeyboardTopPanelComponent: Component { } public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + self.draggingEndOffset = nil + + if let component = self.component { + var focusItemIndex: Int? + + var location = self.scrollView.panGestureRecognizer.location(in: self.scrollView) + let translation = self.scrollView.panGestureRecognizer.translation(in: self.scrollView) + location.x -= translation.x + location.y -= translation.y + + for (id, itemView) in self.itemViews { + if itemView.frame.insetBy(dx: -4.0, dy: -4.0).contains(location) { + inner: for i in 0 ..< component.items.count { + if id == component.items[i].id { + focusItemIndex = i + break inner + } + } + break + } + } + + self.draggingFocusItemIndex = focusItemIndex + } + self.updateIsDragging(true) } public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + self.draggingEndOffset = scrollView.contentOffset.x + + if let component = self.component { + var focusItemIndex: Int? + + var location = self.scrollView.panGestureRecognizer.location(in: self.scrollView) + let translation = self.scrollView.panGestureRecognizer.translation(in: self.scrollView) + location.x -= translation.x + location.y -= translation.y + + for (id, itemView) in self.itemViews { + if itemView.frame.insetBy(dx: -4.0, dy: -4.0).contains(location) { + inner: for i in 0 ..< component.items.count { + if id == component.items[i].id { + focusItemIndex = i + break inner + } + } + break + } + } + + self.draggingFocusItemIndex = focusItemIndex + } + if !decelerate { self.updateIsDragging(false) } @@ -1187,7 +1383,8 @@ final class EntityKeyboardTopPanelComponent: Component { } var visibleBounds = self.scrollView.bounds - visibleBounds.size.width += 200.0 + visibleBounds.origin.x -= 200.0 + visibleBounds.size.width += 400.0 var validIds = Set() let visibleItemRange = itemLayout.visibleItemRange(for: visibleBounds) @@ -1212,7 +1409,7 @@ final class EntityKeyboardTopPanelComponent: Component { transition: itemTransition, component: item.content, environment: { - EntityKeyboardTopPanelItemEnvironment(isExpanded: itemLayout.isExpanded, isHighlighted: self.activeContentItemId == item.id) + EntityKeyboardTopPanelItemEnvironment(isExpanded: itemLayout.isExpanded, isHighlighted: self.activeContentItemId == item.id, highlightedSubgroupId: self.activeContentItemId == item.id ? self.activeSubcontentItemId : nil) }, containerSize: itemOuterFrame.size ) @@ -1259,6 +1456,7 @@ final class EntityKeyboardTopPanelComponent: Component { } if self.isReordering { self.isReordering = false + self.reorderGestureRecognizer?.state = .failed } if self.isDraggingOrReordering { self.isDraggingOrReordering = false @@ -1302,35 +1500,124 @@ final class EntityKeyboardTopPanelComponent: Component { var updatedBounds: CGRect? if wasExpanded != isExpanded, let previousItemLayout = previousItemLayout { + if !isExpanded { + if let draggingEndOffset = self.draggingEndOffset { + if abs(self.scrollView.contentOffset.x - draggingEndOffset) > 16.0 { + self.draggingFocusItemIndex = nil + } + } else { + self.draggingFocusItemIndex = nil + } + } + var visibleBounds = self.scrollView.bounds - visibleBounds.size.width += 200.0 + visibleBounds.origin.x -= 200.0 + visibleBounds.size.width += 400.0 let previousVisibleRange = previousItemLayout.visibleItemRange(for: visibleBounds) if previousVisibleRange.minIndex <= previousVisibleRange.maxIndex { - let previousItemFrame = previousItemLayout.containerFrame(at: previousVisibleRange.minIndex) - let updatedItemFrame = itemLayout.containerFrame(at: previousVisibleRange.minIndex) + var itemIndex = self.draggingFocusItemIndex ?? ((previousVisibleRange.minIndex + previousVisibleRange.maxIndex) / 2) + if !isExpanded { + if self.scrollView.bounds.maxX >= self.scrollView.contentSize.width { + itemIndex = component.items.count - 1 + } + if self.scrollView.bounds.minX <= 0.0 { + itemIndex = 0 + } + } + + var previousItemFrame = previousItemLayout.containerFrame(at: itemIndex) + var updatedItemFrame = itemLayout.containerFrame(at: itemIndex) + + let previousDistanceToItem = (previousItemFrame.minX - self.scrollView.bounds.minX) + let previousDistanceToItemRight = (previousItemFrame.maxX - self.scrollView.bounds.maxX) + var newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - previousDistanceToItem, y: 0.0), size: availableSize) + var useRightAnchor = false + if newBounds.minX > itemLayout.contentSize.width - self.scrollView.bounds.width { + newBounds.origin.x = itemLayout.contentSize.width - self.scrollView.bounds.width + itemIndex = component.items.count - 1 + useRightAnchor = true + } + if newBounds.minX < 0.0 { + newBounds.origin.x = 0.0 + itemIndex = 0 + useRightAnchor = false + } + + if useRightAnchor { + var newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.maxX - previousDistanceToItemRight, y: 0.0), size: availableSize) + if newBounds.minX > itemLayout.contentSize.width - self.scrollView.bounds.width { + newBounds.origin.x = itemLayout.contentSize.width - self.scrollView.bounds.width + } + if newBounds.minX < 0.0 { + } + } + + previousItemFrame = previousItemLayout.containerFrame(at: itemIndex) + updatedItemFrame = itemLayout.containerFrame(at: itemIndex) + + self.draggingFocusItemIndex = itemIndex - let previousDistanceToItem = (previousItemFrame.minX - self.scrollView.bounds.minX)// / previousItemFrame.width - let newBounds = CGRect(origin: CGPoint(x: updatedItemFrame.minX - previousDistanceToItem/* * updatedItemFrame.width)*/, y: 0.0), size: availableSize) updatedBounds = newBounds var updatedVisibleBounds = newBounds - updatedVisibleBounds.size.width += 200.0 + updatedVisibleBounds.origin.x -= 200.0 + updatedVisibleBounds.size.width += 400.0 let updatedVisibleRange = itemLayout.visibleItemRange(for: updatedVisibleBounds) - let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.minX, y: previousItemFrame.minY), size: previousItemFrame.size) - for index in updatedVisibleRange.minIndex ..< updatedVisibleRange.maxIndex { - let indexDifference = index - previousVisibleRange.minIndex - if let itemView = self.itemViews[self.items[index].id] { - let itemContainerOriginX = baseFrame.minX + CGFloat(indexDifference) * (previousItemLayout.itemSize.width + previousItemLayout.itemSpacing) - let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerOriginX, y: baseFrame.minY), size: baseFrame.size) - let itemOuterFrame = previousItemLayout.contentFrame(containerFrame: itemContainerFrame) - - let itemSize = itemView.bounds.size - itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) + if useRightAnchor { + let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.maxX - previousItemFrame.width, y: previousItemFrame.minY), size: previousItemFrame.size) + for index in updatedVisibleRange.minIndex ... updatedVisibleRange.maxIndex { + let indexDifference = index - itemIndex + if let itemView = self.itemViews[self.items[index].id] { + let itemContainerMaxX = baseFrame.maxX + CGFloat(indexDifference) * (previousItemLayout.itemSize.width + previousItemLayout.itemSpacing) + let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerMaxX - baseFrame.width, y: baseFrame.minY), size: baseFrame.size) + let itemOuterFrame = previousItemLayout.contentFrame(index: index, containerFrame: itemContainerFrame) + + let itemSize = itemView.bounds.size + itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) + + if let activeContentItemId = self.activeContentItemId, activeContentItemId == self.items[index].id { + self.highlightedIconBackgroundView.frame = itemOuterFrame + } + } + } + } else { + let baseFrame = CGRect(origin: CGPoint(x: updatedItemFrame.minX, y: previousItemFrame.minY), size: previousItemFrame.size) + for index in updatedVisibleRange.minIndex ... updatedVisibleRange.maxIndex { + let indexDifference = index - itemIndex + if let itemView = self.itemViews[self.items[index].id] { + var itemContainerOriginX = baseFrame.minX + if indexDifference > 0 { + for i in 0 ..< indexDifference { + itemContainerOriginX += previousItemLayout.itemSpacing + itemContainerOriginX += previousItemLayout.containerFrame(at: itemIndex + i).width + } + } else if indexDifference < 0 { + for i in 0 ..< (-indexDifference) { + itemContainerOriginX -= previousItemLayout.itemSpacing + itemContainerOriginX -= previousItemLayout.containerFrame(at: itemIndex - i - 1).width + } + } + + let previousContainerFrame = previousItemLayout.containerFrame(at: index) + let itemContainerFrame = CGRect(origin: CGPoint(x: itemContainerOriginX, y: previousContainerFrame.minY), size: previousContainerFrame.size) + let itemOuterFrame = previousItemLayout.contentFrame(index: index, containerFrame: itemContainerFrame) + + let itemSize = itemView.bounds.size + itemView.frame = CGRect(origin: CGPoint(x: itemOuterFrame.minX + floor((itemOuterFrame.width - itemSize.width) / 2.0), y: itemOuterFrame.minY + floor((itemOuterFrame.height - itemSize.height) / 2.0)), size: itemSize) + + if let activeContentItemId = self.activeContentItemId, activeContentItemId == self.items[index].id { + self.highlightedIconBackgroundView.frame = itemOuterFrame + } + } } } } + + if !isExpanded { + self.draggingFocusItemIndex = nil + } } if self.scrollView.contentSize != itemLayout.contentSize { @@ -1355,7 +1642,13 @@ final class EntityKeyboardTopPanelComponent: Component { highlightTransition = .immediate } - highlightTransition.setCornerRadius(layer: self.highlightedIconBackgroundView.layer, cornerRadius: activeContentItemId.base is String ? min(itemFrame.width / 2.0, itemFrame.height / 2.0) : 10.0) + let isRound: Bool + if let string = activeContentItemId.base as? String, (string == "recent" || string == "static") { + isRound = true + } else { + isRound = false + } + highlightTransition.setCornerRadius(layer: self.highlightedIconBackgroundView.layer, cornerRadius: isRound ? min(itemFrame.width / 2.0, itemFrame.height / 2.0) : 10.0) highlightTransition.setPosition(view: self.highlightedIconBackgroundView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY)) highlightTransition.setBounds(view: self.highlightedIconBackgroundView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) } else { @@ -1373,11 +1666,11 @@ final class EntityKeyboardTopPanelComponent: Component { strongSelf.visibilityFractionUpdated(value: fraction, transition: transition) } - component.activeContentItemIdUpdated.connect { [weak self] (itemId, transition) in + component.activeContentItemIdUpdated.connect { [weak self] (itemId, subcontentItemId, transition) in guard let strongSelf = self else { return } - strongSelf.activeContentItemIdUpdated(itemId: itemId, transition: transition) + strongSelf.activeContentItemIdUpdated(itemId: itemId, subcontentItemId: subcontentItemId, transition: transition) } return CGSize(width: availableSize.width, height: height) @@ -1401,14 +1694,15 @@ final class EntityKeyboardTopPanelComponent: Component { } } - private func activeContentItemIdUpdated(itemId: AnyHashable, transition: Transition) { + private func activeContentItemIdUpdated(itemId: AnyHashable, subcontentItemId: AnyHashable?, transition: Transition) { guard let component = self.component, let itemLayout = self.itemLayout else { return } - if self.activeContentItemId == itemId { + if self.activeContentItemId == itemId && self.activeSubcontentItemId == subcontentItemId { return } self.activeContentItemId = itemId + self.activeSubcontentItemId = subcontentItemId let _ = component let _ = itemLayout diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index d0caa97fcf..3ff0d8f0f8 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -42,12 +42,14 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer { if self.shouldBeAnimating { self.playbackTimer?.invalidate() + let startTimestamp = self.playbackTimestamp + CFAbsoluteTimeGetCurrent() self.playbackTimer = SwiftSignalKit.Timer(timeout: 1.0 / 30.0, repeat: true, completion: { [weak self] in guard let strongSelf = self else { return } - strongSelf.frameManager?.tick(timestamp: strongSelf.playbackTimestamp) - strongSelf.playbackTimestamp += 1.0 / 30.0 + let timestamp = CFAbsoluteTimeGetCurrent() - startTimestamp + strongSelf.frameManager?.tick(timestamp: timestamp) + strongSelf.playbackTimestamp = timestamp }, queue: .mainQueue()) self.playbackTimer?.start() } else { @@ -560,7 +562,7 @@ public final class GifPagerContentComponent: Component { } private func updateScrollingOffset(transition: Transition) { - let isInteracting = scrollView.isDragging || scrollView.isTracking || scrollView.isDecelerating + let isInteracting = scrollView.isDragging || scrollView.isDecelerating if let previousScrollingOffsetValue = self.previousScrollingOffset { let currentBounds = scrollView.bounds let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) diff --git a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift index 9cac2ad1a3..bc09070b5b 100644 --- a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift +++ b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift @@ -14,14 +14,27 @@ public func cacheLottieAnimation(data: Data, width: Int, height: Int, writer: An return } - let frameDuration = 1.0 / Double(animation.frameRate) - for i in 0 ..< animation.frameCount { + + let frameSkip: Int + if animation.frameRate >= 60 { + if ProcessInfo.processInfo.activeProcessorCount > 2 { + frameSkip = 1 + } else { + frameSkip = 2 + } + } else { + frameSkip = 1 + } + + let frameDuration = Double(frameSkip) / Double(animation.frameRate) + for i in stride(from: 0, through: animation.frameCount - 1, by: frameSkip) { if writer.isCancelled { break } writer.add(with: { surface in animation.renderFrame(with: i, into: surface.argb, width: Int32(surface.width), height: Int32(surface.height), bytesPerRow: Int32(surface.bytesPerRow)) - }, proposedWidth: width, proposedHeight: height, duration: frameDuration) + return frameDuration + }, proposedWidth: width, proposedHeight: height) } writer.finish() @@ -39,7 +52,8 @@ public func cacheStillSticker(path: String, width: Int, height: Int, writer: Ani UIGraphicsPopContext() } memcpy(surface.argb, context.bytes, surface.height * surface.bytesPerRow) - }, proposedWidth: width, proposedHeight: height, duration: 1.0) + return 1.0 + }, proposedWidth: width, proposedHeight: height) } writer.finish() diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift index 2a1c71affb..df715c5a18 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift @@ -249,14 +249,18 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { } private final class Frame { - let timestamp: Double + let duration: Double let textureY: TextureStorage let textureU: TextureStorage let textureV: TextureStorage let textureA: TextureStorage - init?(device: MTLDevice, textureY: TextureStorage, textureU: TextureStorage, textureV: TextureStorage, textureA: TextureStorage, data: AnimationCacheItemFrame, timestamp: Double) { - self.timestamp = timestamp + var remainingDuration: Double + + init?(device: MTLDevice, textureY: TextureStorage, textureU: TextureStorage, textureV: TextureStorage, textureA: TextureStorage, data: AnimationCacheItemFrame, duration: Double) { + self.duration = duration + self.remainingDuration = duration + self.textureY = textureY self.textureU = textureU self.textureV = textureV @@ -281,7 +285,6 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { private let stateUpdated: () -> Void private var disposable: Disposable? - private var timestamp: Double = 0.0 private var item: AnimationCacheItem? private(set) var currentFrame: Frame? @@ -354,18 +357,35 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { return self.update(device: device, texturePoolFullPlane: texturePoolFullPlane, texturePoolHalfPlane: texturePoolHalfPlane, advanceTimestamp: advanceTimestamp) } - private func update(device: MTLDevice, texturePoolFullPlane: TextureStoragePool, texturePoolHalfPlane: TextureStoragePool, advanceTimestamp: Double?) -> LoadFrameTask? { + private func update(device: MTLDevice, texturePoolFullPlane: TextureStoragePool, texturePoolHalfPlane: TextureStoragePool, advanceTimestamp: Double) -> LoadFrameTask? { guard let item = self.item else { return nil } - let timestamp = self.timestamp - if let advanceTimestamp = advanceTimestamp { - self.timestamp += advanceTimestamp + if let currentFrame = self.currentFrame, !self.isLoadingFrame { + currentFrame.remainingDuration -= advanceTimestamp } - if let currentFrame = self.currentFrame, currentFrame.timestamp == self.timestamp { - } else if !self.isLoadingFrame { + var frameAdvance: AnimationCacheItem.Advance? + if !self.isLoadingFrame { + if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 { + let divisionFactor = advanceTimestamp / currentFrame.remainingDuration + let wholeFactor = round(divisionFactor) + if abs(wholeFactor - divisionFactor) < 0.005 { + currentFrame.remainingDuration = 0.0 + frameAdvance = .frames(Int(wholeFactor)) + } else { + currentFrame.remainingDuration -= advanceTimestamp + if currentFrame.remainingDuration <= 0.0 { + frameAdvance = .duration(currentFrame.duration + max(0.0, -currentFrame.remainingDuration)) + } + } + } else if self.currentFrame == nil { + frameAdvance = .frames(1) + } + } + + if let frameAdvance = frameAdvance, !self.isLoadingFrame { self.isLoadingFrame = true let fullParameters = texturePoolFullPlane.parameters @@ -378,7 +398,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { let preferredRowAlignment = self.preferredRowAlignment return LoadFrameTask(task: { [weak self] in - let frame = item.getFrame(at: timestamp, requestedFormat: .yuva(rowAlignment: preferredRowAlignment)) + let frame = item.advance(advance: frameAdvance, requestedFormat: .yuva(rowAlignment: preferredRowAlignment)) let textureY = readyTextureY ?? TextureStoragePool.takeNew(device: device, parameters: fullParameters, pool: texturePoolFullPlane) let textureU = readyTextureU ?? TextureStoragePool.takeNew(device: device, parameters: halfParameters, pool: texturePoolHalfPlane) @@ -387,7 +407,7 @@ public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { var currentFrame: Frame? if let frame = frame, let textureY = textureY, let textureU = textureU, let textureV = textureV, let textureA = textureA { - currentFrame = Frame(device: device, textureY: textureY, textureU: textureU, textureV: textureV, textureA: textureA, data: frame, timestamp: timestamp) + currentFrame = Frame(device: device, textureY: textureY, textureU: textureU, textureV: textureV, textureA: textureA, data: frame, duration: frame.duration) } return { diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index 08a52b35db..ffe00bae70 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -29,6 +29,17 @@ open class MultiAnimationRenderTarget: SimpleLayer { } } + public var blurredRepresentationBackgroundColor: UIColor? + public var blurredRepresentationTarget: CALayer? { + didSet { + if self.blurredRepresentationTarget !== oldValue { + for f in self.updateStateCallbacks.copyItems() { + f() + } + } + } + } + public override init() { assert(Thread.isMainThread) @@ -65,60 +76,6 @@ open class MultiAnimationRenderTarget: SimpleLayer { } } -private final class FrameGroup { - let image: UIImage - let badgeImage: UIImage? - let size: CGSize - let timestamp: Double - - init?(item: AnimationCacheItem, timestamp: Double) { - guard let firstFrame = item.getFrame(at: timestamp, requestedFormat: .rgba) else { - return nil - } - - switch firstFrame.format { - case let .rgba(data, width, height, bytesPerRow): - let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) - - data.withUnsafeBytes { bytes -> Void in - memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) - - /*var sourceBuffer = vImage_Buffer() - sourceBuffer.width = UInt(width) - sourceBuffer.height = UInt(height) - sourceBuffer.data = UnsafeMutableRawPointer(mutating: bytes.baseAddress!.advanced(by: firstFrame.range.lowerBound)) - sourceBuffer.rowBytes = bytesPerRow - - var destinationBuffer = vImage_Buffer() - destinationBuffer.width = UInt(32) - destinationBuffer.height = UInt(32) - destinationBuffer.data = context.bytes - destinationBuffer.rowBytes = bytesPerRow - - vImageBoxConvolve_ARGB8888(&sourceBuffer, - &destinationBuffer, - nil, - UInt(width - 32 - 16), UInt(height - 32 - 16), - UInt32(31), - UInt32(31), - nil, - vImage_Flags(kvImageEdgeExtend))*/ - } - - guard let image = context.generateImage() else { - return nil - } - - self.image = image - self.size = CGSize(width: CGFloat(width), height: CGFloat(height)) - self.timestamp = timestamp - self.badgeImage = nil - default: - return nil - } - } -} - private final class LoadFrameGroupTask { let task: () -> () -> Void @@ -128,6 +85,190 @@ private final class LoadFrameGroupTask { } private final class ItemAnimationContext { + fileprivate final class Frame { + let frame: AnimationCacheItemFrame + let duration: Double + let image: UIImage + let badgeImage: UIImage? + let size: CGSize + + var remainingDuration: Double + + private var blurredRepresentationValue: UIImage? + + init?(frame: AnimationCacheItemFrame) { + self.frame = frame + self.duration = frame.duration + self.remainingDuration = frame.duration + + switch frame.format { + case let .rgba(data, width, height, bytesPerRow): + let context = DrawingContext(size: CGSize(width: CGFloat(width), height: CGFloat(height)), scale: 1.0, opaque: false, bytesPerRow: bytesPerRow) + + data.withUnsafeBytes { bytes -> Void in + memcpy(context.bytes, bytes.baseAddress!, height * bytesPerRow) + + /*var sourceBuffer = vImage_Buffer() + sourceBuffer.width = UInt(width) + sourceBuffer.height = UInt(height) + sourceBuffer.data = UnsafeMutableRawPointer(mutating: bytes.baseAddress!.advanced(by: firstFrame.range.lowerBound)) + sourceBuffer.rowBytes = bytesPerRow + + var destinationBuffer = vImage_Buffer() + destinationBuffer.width = UInt(32) + destinationBuffer.height = UInt(32) + destinationBuffer.data = context.bytes + destinationBuffer.rowBytes = bytesPerRow + + vImageBoxConvolve_ARGB8888(&sourceBuffer, + &destinationBuffer, + nil, + UInt(width - 32 - 16), UInt(height - 32 - 16), + UInt32(31), + UInt32(31), + nil, + vImage_Flags(kvImageEdgeExtend))*/ + } + + guard let image = context.generateImage() else { + return nil + } + + self.image = image + self.size = CGSize(width: CGFloat(width), height: CGFloat(height)) + self.badgeImage = nil + default: + return nil + } + } + + func blurredRepresentation(color: UIColor?) -> UIImage? { + if let blurredRepresentationValue = self.blurredRepresentationValue { + return blurredRepresentationValue + } + + switch frame.format { + case let .rgba(data, width, height, bytesPerRow): + let blurredWidth = 12 + let blurredHeight = 12 + let context = DrawingContext(size: CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)), scale: 1.0, opaque: true, bytesPerRow: bytesPerRow) + + data.withUnsafeBytes { bytes -> Void in + if let dataProvider = CGDataProvider(dataInfo: nil, data: bytes.baseAddress!, size: bytes.count, releaseData: { _, _, _ in }) { + let image = CGImage( + width: width, + height: height, + bitsPerComponent: 8, + bitsPerPixel: 32, + bytesPerRow: bytesPerRow, + space: DeviceGraphicsContextSettings.shared.colorSpace, + bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo, + provider: dataProvider, + decode: nil, + shouldInterpolate: true, + intent: .defaultIntent + ) + if let image = image { + let size = CGSize(width: CGFloat(blurredWidth), height: CGFloat(blurredHeight)) + context.withFlippedContext { c in + c.setFillColor((color ?? .white).cgColor) + c.fill(CGRect(origin: CGPoint(), size: size)) + c.draw(image, in: CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), size: CGSize(width: size.width * 1.8, height: size.height * 1.8))) + } + } + } + + var destinationBuffer = vImage_Buffer() + destinationBuffer.width = UInt(blurredWidth) + destinationBuffer.height = UInt(blurredHeight) + destinationBuffer.data = context.bytes + destinationBuffer.rowBytes = context.bytesPerRow + + /*var sourceBuffer = vImage_Buffer() + sourceBuffer.width = UInt(width) + sourceBuffer.height = UInt(height) + sourceBuffer.data = UnsafeMutableRawPointer(mutating: bytes.baseAddress!) + sourceBuffer.rowBytes = bytesPerRow + + let tempBufferBytes = malloc(blurredHeight * context.bytesPerRow) + defer { + free(tempBufferBytes) + } + let temp2BufferBytes = malloc(blurredHeight * context.bytesPerRow) + defer { + free(temp2BufferBytes) + } + memset(temp2BufferBytes, Int32(bitPattern: color?.argb ?? 0xffffffff), blurredHeight * context.bytesPerRow) + + var tempBuffer = vImage_Buffer() + tempBuffer.width = UInt(blurredWidth) + tempBuffer.height = UInt(blurredHeight) + tempBuffer.data = tempBufferBytes + tempBuffer.rowBytes = context.bytesPerRow + + var temp2Buffer = vImage_Buffer() + temp2Buffer.width = UInt(blurredWidth) + temp2Buffer.height = UInt(blurredHeight) + temp2Buffer.data = temp2BufferBytes + temp2Buffer.rowBytes = context.bytesPerRow + + + + vImageScale_ARGB8888(&sourceBuffer, &tempBuffer, nil, vImage_Flags(kvImageDoNotTile)) + //vImageUnpremultiplyData_ARGB8888(&tempBuffer, &tempBuffer, vImage_Flags(kvImageDoNotTile)) + + vImagePremultipliedAlphaBlend_ARGB8888(&tempBuffer, &temp2Buffer, &destinationBuffer, vImage_Flags(kvImageDoNotTile)) + //vImageCopyBuffer(&tempBuffer, &destinationBuffer, 4, vImage_Flags(kvImageDoNotTile))*/ + + vImageBoxConvolve_ARGB8888(&destinationBuffer, + &destinationBuffer, + nil, + 0, 0, + UInt32(15), + UInt32(15), + nil, + vImage_Flags(kvImageTruncateKernel)) + + let divisor: Int32 = 0x1000 + + let rwgt: CGFloat = 0.3086 + let gwgt: CGFloat = 0.6094 + let bwgt: CGFloat = 0.0820 + + let adjustSaturation: CGFloat = 1.7 + + let a = (1.0 - adjustSaturation) * rwgt + adjustSaturation + let b = (1.0 - adjustSaturation) * rwgt + let c = (1.0 - adjustSaturation) * rwgt + let d = (1.0 - adjustSaturation) * gwgt + let e = (1.0 - adjustSaturation) * gwgt + adjustSaturation + let f = (1.0 - adjustSaturation) * gwgt + let g = (1.0 - adjustSaturation) * bwgt + let h = (1.0 - adjustSaturation) * bwgt + let i = (1.0 - adjustSaturation) * bwgt + adjustSaturation + + let satMatrix: [CGFloat] = [ + a, b, c, 0, + d, e, f, 0, + g, h, i, 0, + 0, 0, 0, 1 + ] + + var matrix: [Int16] = satMatrix.map { value in + return Int16(value * CGFloat(divisor)) + } + + vImageMatrixMultiply_ARGB8888(&destinationBuffer, &destinationBuffer, &matrix, divisor, nil, nil, vImage_Flags(kvImageDoNotTile)) + } + + self.blurredRepresentationValue = context.generateImage() + return self.blurredRepresentationValue + default: + return nil + } + } + } + static let queue = Queue(name: "ItemAnimationContext", qos: .default) private let cache: AnimationCache @@ -135,11 +276,10 @@ private final class ItemAnimationContext { private var disposable: Disposable? private var displayLink: ConstantDisplayLinkAnimator? - private var timestamp: Double = 0.0 private var item: AnimationCacheItem? - private var currentFrameGroup: FrameGroup? - private var isLoadingFrameGroup: Bool = false + private var currentFrame: Frame? + private var isLoadingFrame: Bool = false private(set) var isPlaying: Bool = false { didSet { @@ -180,9 +320,13 @@ private final class ItemAnimationContext { } func updateAddedTarget(target: MultiAnimationRenderTarget) { - if let currentFrameGroup = self.currentFrameGroup { - if let cgImage = currentFrameGroup.image.cgImage { + if let currentFrame = self.currentFrame { + if let cgImage = currentFrame.image.cgImage { target.transitionToContents(cgImage) + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } } } @@ -215,35 +359,57 @@ private final class ItemAnimationContext { return self.update(advanceTimestamp: advanceTimestamp) } - private func update(advanceTimestamp: Double?) -> LoadFrameGroupTask? { + private func update(advanceTimestamp: Double) -> LoadFrameGroupTask? { guard let item = self.item else { return nil } - let timestamp = self.timestamp - if let advanceTimestamp = advanceTimestamp { - self.timestamp += advanceTimestamp + var frameAdvance: AnimationCacheItem.Advance? + if !self.isLoadingFrame { + if let currentFrame = self.currentFrame, advanceTimestamp > 0.0 { + let divisionFactor = advanceTimestamp / currentFrame.remainingDuration + let wholeFactor = round(divisionFactor) + if abs(wholeFactor - divisionFactor) < 0.005 { + currentFrame.remainingDuration = 0.0 + frameAdvance = .frames(Int(wholeFactor)) + } else { + currentFrame.remainingDuration -= advanceTimestamp + if currentFrame.remainingDuration <= 0.0 { + frameAdvance = .duration(currentFrame.duration + max(0.0, -currentFrame.remainingDuration)) + } + } + } else if self.currentFrame == nil { + frameAdvance = .frames(1) + } } - if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.timestamp == self.timestamp { - } else if !self.isLoadingFrameGroup { - self.isLoadingFrameGroup = true + if let frameAdvance = frameAdvance, !self.isLoadingFrame { + self.isLoadingFrame = true return LoadFrameGroupTask(task: { [weak self] in - let currentFrameGroup = FrameGroup(item: item, timestamp: timestamp) + let currentFrame: Frame? + if let frame = item.advance(advance: frameAdvance, requestedFormat: .rgba) { + currentFrame = Frame(frame: frame) + } else { + currentFrame = nil + } return { guard let strongSelf = self else { return } - strongSelf.isLoadingFrameGroup = false + strongSelf.isLoadingFrame = false - if let currentFrameGroup = currentFrameGroup { - strongSelf.currentFrameGroup = currentFrameGroup + if let currentFrame = currentFrame { + strongSelf.currentFrame = currentFrame for target in strongSelf.targets.copyItems() { if let target = target.value { - target.transitionToContents(currentFrameGroup.image.cgImage!) + target.transitionToContents(currentFrame.image.cgImage!) + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = currentFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } } } } @@ -251,7 +417,7 @@ private final class ItemAnimationContext { }) } - if let _ = self.currentFrameGroup { + if let _ = self.currentFrame { for target in self.targets.copyItems() { if let target = target.value { target.updateDisplayPlaceholder(displayPlaceholder: false) @@ -268,7 +434,13 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { private let firstFrameQueue: Queue private let stateUpdated: () -> Void - private var itemContexts: [String: ItemAnimationContext] = [:] + private struct ItemKey: Hashable { + var id: String + var width: Int + var height: Int + } + + private var itemContexts: [ItemKey: ItemAnimationContext] = [:] private(set) var isPlaying: Bool = false { didSet { @@ -284,8 +456,9 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { + let itemKey = ItemKey(id: itemId, width: Int(size.width), height: Int(size.height)) let itemContext: ItemAnimationContext - if let current = self.itemContexts[itemId] { + if let current = self.itemContexts[itemKey] { itemContext = current } else { itemContext = ItemAnimationContext(cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in @@ -294,7 +467,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } strongSelf.updateIsPlaying() }) - self.itemContexts[itemId] = itemContext + self.itemContexts[itemKey] = itemContext } let index = itemContext.targets.add(Weak(target)) @@ -302,12 +475,12 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in Queue.mainQueue().async { - guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else { + guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { return } itemContext.targets.remove(index) if itemContext.targets.isEmpty { - strongSelf.itemContexts.removeValue(forKey: itemId) + strongSelf.itemContexts.removeValue(forKey: itemKey) } } } @@ -320,7 +493,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } return ActionDisposable { [weak self, weak itemContext, weak target] in - guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else { + guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemKey] === itemContext else { return } if let target = target { @@ -329,18 +502,25 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } itemContext.targets.remove(index) if itemContext.targets.isEmpty { - strongSelf.itemContexts.removeValue(forKey: itemId) + strongSelf.itemContexts.removeValue(forKey: itemKey) } } } func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { if let item = cache.getFirstFrameSynchronously(sourceId: itemId, size: size) { - guard let frameGroup = FrameGroup(item: item, timestamp: 0.0) else { + guard let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) else { + return false + } + guard let loadedFrame = ItemAnimationContext.Frame(frame: frame) else { return false } - target.contents = frameGroup.image.cgImage + target.contents = loadedFrame.image.cgImage + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } return true } else { @@ -357,15 +537,24 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { return } - let frameGroup = FrameGroup(item: item, timestamp: 0.0) + let loadedFrame: ItemAnimationContext.Frame? + if let frame = item.advance(advance: .frames(1), requestedFormat: .rgba) { + loadedFrame = ItemAnimationContext.Frame(frame: frame) + } else { + loadedFrame = nil + } Queue.mainQueue().async { guard let target = target else { completion(false) return } - if let frameGroup = frameGroup { - target.contents = frameGroup.image.cgImage + if let loadedFrame = loadedFrame { + target.contents = loadedFrame.image.cgImage + + if let blurredRepresentationTarget = target.blurredRepresentationTarget { + blurredRepresentationTarget.contents = loadedFrame.blurredRepresentation(color: target.blurredRepresentationBackgroundColor)?.cgImage + } completion(true) } else { diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 97d36a4020..736613287a 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -226,7 +226,7 @@ public final class TextNodeWithEntities { if let current = self.inlineStickerItemLayers[id] { itemLayer = current } else { - itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize)) + itemLayer = InlineStickerItemLayer(context: context, attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: floor(itemSize * 1.2), height: floor(itemSize * 1.2))) self.inlineStickerItemLayers[id] = itemLayer self.textNode.layer.addSublayer(itemLayer) diff --git a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift index ddd56772f7..c59db57746 100644 --- a/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift +++ b/submodules/TelegramUI/Components/VideoAnimationCache/Sources/VideoAnimationCache.swift @@ -44,7 +44,8 @@ public func cacheVideoAnimation(path: String, width: Int, height: Int, writer: A } } } - }, proposedWidth: frame.width, proposedHeight: frame.height, duration: frameDuration) + return frameDuration + }, proposedWidth: frame.width, proposedHeight: frame.height) } else { break } diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeAdd.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeAdd.imageset/Contents.json new file mode 100644 index 0000000000..776f120f42 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeAdd.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "addstickers.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeAdd.imageset/addstickers.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeAdd.imageset/addstickers.pdf new file mode 100644 index 0000000000..bf0428ec3f --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeAdd.imageset/addstickers.pdf @@ -0,0 +1,103 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 8.000000 2.000000 cm +0.000000 0.000000 0.000000 scn +1.000000 10.000000 m +1.000000 10.552285 0.552285 11.000000 0.000000 11.000000 c +-0.552285 11.000000 -1.000000 10.552285 -1.000000 10.000000 c +1.000000 10.000000 l +h +-1.000000 2.000000 m +-1.000000 1.447715 -0.552285 1.000000 0.000000 1.000000 c +0.552285 1.000000 1.000000 1.447715 1.000000 2.000000 c +-1.000000 2.000000 l +h +-1.000000 10.000000 m +-1.000000 2.000000 l +1.000000 2.000000 l +1.000000 10.000000 l +-1.000000 10.000000 l +h +f +n +Q +q +0.000000 1.000000 -1.000000 0.000000 6.000000 8.000000 cm +0.000000 0.000000 0.000000 scn +1.000000 2.000000 m +1.000000 2.552285 0.552285 3.000000 0.000000 3.000000 c +-0.552285 3.000000 -1.000000 2.552285 -1.000000 2.000000 c +1.000000 2.000000 l +h +-1.000000 -6.000000 m +-1.000000 -6.552285 -0.552285 -7.000000 0.000000 -7.000000 c +0.552285 -7.000000 1.000000 -6.552285 1.000000 -6.000000 c +-1.000000 -6.000000 l +h +-1.000000 2.000000 m +-1.000000 -6.000000 l +1.000000 -6.000000 l +1.000000 2.000000 l +-1.000000 2.000000 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 1083 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 16.000000 16.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001173 00000 n +0000001196 00000 n +0000001369 00000 n +0000001443 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1502 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeLock.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeLock.imageset/Contents.json new file mode 100644 index 0000000000..fede63e12b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeLock.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "lockedstickers.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeLock.imageset/lockedstickers.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeLock.imageset/lockedstickers.pdf new file mode 100644 index 0000000000..554f3795b0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/PanelBadgeLock.imageset/lockedstickers.pdf @@ -0,0 +1,93 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.000000 2.999512 cm +0.000000 0.000000 0.000000 scn +2.165000 7.500039 m +2.165000 8.513481 2.986557 9.335039 4.000000 9.335039 c +5.013443 9.335039 5.835000 8.513481 5.835000 7.500039 c +5.835000 5.996800 l +5.637455 6.000156 5.413168 6.000156 5.155556 6.000156 c +2.844445 6.000156 l +2.586832 6.000156 2.362546 6.000156 2.165000 5.996800 c +2.165000 7.500039 l +h +0.835000 5.729585 m +0.835000 7.500039 l +0.835000 9.248020 2.252019 10.665039 4.000000 10.665039 c +5.747981 10.665039 7.165000 9.248020 7.165000 7.500039 c +7.165000 5.729585 l +7.437430 5.559180 7.659470 5.317513 7.806234 5.029473 c +8.000000 4.649185 8.000000 4.151361 8.000000 3.155712 c +8.000000 2.844601 l +8.000000 1.848951 8.000000 1.351128 7.806234 0.970840 c +7.635793 0.636330 7.363827 0.364364 7.029317 0.193922 c +6.649029 0.000156 6.151205 0.000156 5.155556 0.000156 c +2.844444 0.000156 l +1.848796 0.000156 1.350971 0.000156 0.970684 0.193922 c +0.636173 0.364364 0.364208 0.636330 0.193766 0.970840 c +0.000000 1.351128 0.000000 1.848951 0.000000 2.844601 c +0.000000 3.155712 l +0.000000 4.151361 0.000000 4.649185 0.193766 5.029473 c +0.340530 5.317513 0.562570 5.559180 0.835000 5.729585 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1229 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 16.000000 16.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001319 00000 n +0000001342 00000 n +0000001515 00000 n +0000001589 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1648 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift index 3a81998162..d53796a5e9 100644 --- a/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatContextResultPeekContentNode.swift @@ -147,6 +147,10 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont } } + func ready() -> Signal { + return .single(true) + } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let imageLayout = self.imageNode.asyncLayout() let currentImageResource = self.currentImageResource diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 917b839354..764f39c8f9 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -8606,6 +8606,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode) } + + strongSelf.chatDisplayNode.updateTypingActivity(true) }, backwardsDeleteText: { [weak self] in guard let strongSelf = self else { return diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 016c32deda..fefc2817bb 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2836,12 +2836,21 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if peerId?.namespace != Namespaces.Peer.SecretChat, let interactiveEmojis = self.interactiveEmojis, interactiveEmojis.emojis.contains(trimmedInputText) { messages.append(.message(text: "", attributes: [], inlineStickers: [:], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice(emoji: trimmedInputText)), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil)) } else { + var inlineStickers: [MediaId: Media] = [:] + effectiveInputText.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: effectiveInputText.length), using: { value, _, _ in + if let value = value as? ChatTextInputTextCustomEmojiAttribute { + if let file = value.file { + inlineStickers[file.fileId] = file + } + } + }) + let inputText = convertMarkdownToAttributes(effectiveInputText) for text in breakChatInputText(trimChatInputText(inputText)) { if text.length != 0 { var attributes: [MessageAttribute] = [] - let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: 0/*Int(self.context.userLimits.maxAnimatedEmojisInText)*/)) + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text, maxAnimatedEmojisInText: 0)) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } @@ -2852,17 +2861,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { webpage = self.chatPresentationInterfaceState.urlPreview?.1 } - messages.append(.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil)) - - #if DEBUG - if text.string == "sleep" { - messages.removeAll() - - for i in 0 ..< 5 { - messages.append(.message(text: "sleep\(i)", attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)) - } - } - #endif + messages.append(.message(text: text.string, attributes: attributes, inlineStickers: inlineStickers, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil)) } } diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index 22b3258fb6..4aa93627c8 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -79,19 +79,66 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { let isPremiumDisabled = premiumConfiguration.isPremiumDisabled let emojiItems: Signal = combineLatest( - context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), - hasPremium + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.LocalRecentEmoji], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), + hasPremium, + context.account.viewTracker.featuredEmojiPacks() ) - |> map { view, hasPremium -> EmojiPagerContentComponent in + |> map { view, hasPremium, featuredEmojiPacks -> EmojiPagerContentComponent in struct ItemGroup { var supergroupId: AnyHashable var id: AnyHashable - var isPremium: Bool + var title: String + var isPremiumLocked: Bool + var isFeatured: Bool var items: [EmojiPagerContentComponent.Item] } var itemGroups: [ItemGroup] = [] var itemGroupIndexById: [AnyHashable: Int] = [:] + var recentEmoji: OrderedItemListView? + for orderedView in view.orderedItemListsViews { + if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji { + recentEmoji = orderedView + } + } + + if let recentEmoji = recentEmoji { + for item in recentEmoji.items { + guard let item = item.contents.get(RecentEmojiItem.self) else { + continue + } + + if case let .file(file) = item.content, isPremiumDisabled, file.isPremiumEmoji { + continue + } + + let resultItem: EmojiPagerContentComponent.Item + switch item.content { + case let .file(file): + resultItem = EmojiPagerContentComponent.Item( + file: file, + staticEmoji: nil, + subgroupId: nil + ) + case let .text(text): + resultItem = EmojiPagerContentComponent.Item( + file: nil, + staticEmoji: text, + subgroupId: nil + ) + } + + let groupId = "recent" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + //TODO:localize + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Recently Used", isPremiumLocked: false, isFeatured: false, items: [resultItem])) + } + } + } + for (subgroupId, list) in staticEmojiMapping { let groupId: AnyHashable = "static" for emojiString in list { @@ -105,11 +152,17 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, isPremium: false, items: [resultItem])) + //TODO:localize + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Emoji", isPremiumLocked: false, isFeatured: false, items: [resultItem])) } } } + var installedCollectionIds = Set() + for (id, _, _) in view.collectionInfos { + installedCollectionIds.insert(id) + } + for entry in view.entries { guard let item = entry.item as? StickerPackItem else { continue @@ -122,20 +175,50 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { let supergroupId = entry.index.collectionId let groupId: AnyHashable = supergroupId - let isPremium: Bool = item.file.isPremiumEmoji && !hasPremium - if isPremium && isPremiumDisabled { + let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium + if isPremiumLocked && isPremiumDisabled { continue } - /*if isPremium { - groupId = "\(supergroupId)-p" - } else { - groupId = supergroupId - }*/ if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, isPremium: isPremium, items: [resultItem])) + + var title = "" + inner: for (id, info, _) in view.collectionInfos { + if id == entry.index.collectionId, let info = info as? StickerPackCollectionInfo { + title = info.title + break inner + } + } + itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, isPremiumLocked: isPremiumLocked, isFeatured: false, items: [resultItem])) + } + } + + for featuredEmojiPack in featuredEmojiPacks { + if installedCollectionIds.contains(featuredEmojiPack.info.id) { + continue + } + + for item in featuredEmojiPack.topItems { + let resultItem = EmojiPagerContentComponent.Item( + file: item.file, + staticEmoji: nil, + subgroupId: nil + ) + + let supergroupId = featuredEmojiPack.info.id + let groupId: AnyHashable = supergroupId + let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium + if isPremiumLocked && isPremiumDisabled { + continue + } + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, isPremiumLocked: isPremiumLocked, isFeatured: true, items: [resultItem])) + } } } @@ -146,20 +229,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationRenderer: animationRenderer, inputInteraction: inputInteraction, itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in - var title: String? + var hasClear = false if group.id == AnyHashable("recent") { - //TODO:localize - title = "Recently Used" - } else { - for (id, info, _) in view.collectionInfos { - if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo { - title = info.title - break - } - } + hasClear = true } - return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: title, isPremium: group.isPremium, displayPremiumBadges: false, items: group.items) + return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: group.title, isFeatured: group.isFeatured, isPremiumLocked: group.isPremiumLocked, hasClear: hasClear, displayPremiumBadges: false, items: group.items) }, itemLayoutType: .compact ) @@ -244,12 +319,53 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { }, openStickerSettings: { }, - openPremiumSection: { [weak controllerInteraction] in + addGroupAction: { [weak controllerInteraction] groupId, isPremiumLocked in + guard let controllerInteraction = controllerInteraction, let collectionId = groupId.base as? ItemCollectionId else { + return + } + + if isPremiumLocked { + let controller = PremiumIntroScreen(context: context, source: .stickers) + controllerInteraction.navigationController()?.pushViewController(controller) + + return + } + + let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks) + let _ = (context.account.postbox.combinedView(keys: [viewKey]) + |> take(1) + |> deliverOnMainQueue).start(next: { views in + guard let view = views.views[viewKey] as? OrderedItemListView else { + return + } + for featuredEmojiPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { + if featuredEmojiPack.info.id == collectionId { + let _ = context.engine.stickers.addStickerPackInteractively(info: featuredEmojiPack.info, items: featuredEmojiPack.topItems).start() + + break + } + } + }) + }, + clearGroup: { [weak controllerInteraction] groupId in guard let controllerInteraction = controllerInteraction else { return } - let controller = PremiumIntroScreen(context: context, source: .stickers) - controllerInteraction.navigationController()?.pushViewController(controller) + if groupId == AnyHashable("recent") { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) + var items: [ActionSheetItem] = [] + items.append(ActionSheetButtonItem(title: presentationData.strings.Emoji_ClearRecent, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + let _ = context.engine.stickers.clearRecentlyUsedEmoji().start() + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + controllerInteraction.presentController(actionSheet, nil) + } }, pushController: { [weak controllerInteraction] controller in guard let controllerInteraction = controllerInteraction else { @@ -311,7 +427,27 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { controller.navigationPresentation = .modal controllerInteraction.navigationController()?.pushViewController(controller) }, - openPremiumSection: { + addGroupAction: { _, _ in + }, + clearGroup: { [weak controllerInteraction] groupId in + guard let controllerInteraction = controllerInteraction else { + return + } + if groupId == AnyHashable("recent") { + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) + var items: [ActionSheetItem] = [] + items.append(ActionSheetButtonItem(title: presentationData.strings.Stickers_ClearRecent, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + let _ = context.engine.stickers.clearRecentlyUsedStickers().start() + })) + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + controllerInteraction.presentController(actionSheet, nil) + } }, pushController: { [weak controllerInteraction] controller in guard let controllerInteraction = controllerInteraction else { @@ -366,6 +502,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { struct ItemGroup { var supergroupId: AnyHashable var id: AnyHashable + var title: String + var isPremiumLocked: Bool + var isFeatured: Bool var displayPremiumBadges: Bool var items: [EmojiPagerContentComponent.Item] } @@ -408,7 +547,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: false, items: [resultItem])) + //TODO:localize + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Saved", isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem])) } } } @@ -434,7 +574,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: false, items: [resultItem])) + //TODO:localize + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Recently Used", isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem])) } count += 1 @@ -483,7 +624,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: false, items: [resultItem])) + //TODO:localize + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: "Premium", isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, items: [resultItem])) } } } @@ -502,7 +644,15 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: true, items: [resultItem])) + + var title = "" + inner: for (id, info, _) in view.collectionInfos { + if id == groupId, let info = info as? StickerPackCollectionInfo { + title = info.title + break inner + } + } + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: true, items: [resultItem])) } } @@ -513,26 +663,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationRenderer: animationRenderer, inputInteraction: stickerInputInteraction, itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in - var title: String? - if group.id == AnyHashable("saved") { - //TODO:localize - title = "Saved" - } else if group.id == AnyHashable("recent") { - //TODO:localize - title = "Recently Used" - } else if group.id == AnyHashable("premium") { - //TODO:localize - title = "Premium" - } else { - for (id, info, _) in view.collectionInfos { - if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo { - title = info.title - break - } - } + var hasClear = false + if group.id == AnyHashable("recent") { + hasClear = true } - return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: title, isPremium: false, displayPremiumBadges: group.displayPremiumBadges, items: group.items) + return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: group.title, isFeatured: group.isFeatured, isPremiumLocked: group.isPremiumLocked, hasClear: hasClear, displayPremiumBadges: group.displayPremiumBadges, items: group.items) }, itemLayoutType: .detailed ) @@ -592,6 +728,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { loadMoreToken: nil )) + let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings return combineLatest(queue: .mainQueue(), emojiItems, stickerItems, @@ -603,7 +740,37 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] = [] for reaction in reactions { if let file = animatedEmojiStickers[reaction]?.first?.file { - availableGifSearchEmojies.append(EntityKeyboardComponent.GifSearchEmoji(emoji: reaction, file: file, title: reaction)) + var title: String? + switch reaction { + case "😡": + title = strings.Gif_Emotion_Angry + case "😮": + title = strings.Gif_Emotion_Surprised + case "😂": + title = strings.Gif_Emotion_Joy + case "😘": + title = strings.Gif_Emotion_Kiss + case "😍": + title = strings.Gif_Emotion_Hearts + case "👍": + title = strings.Gif_Emotion_ThumbsUp + case "👎": + title = strings.Gif_Emotion_ThumbsDown + case "🙄": + title = strings.Gif_Emotion_RollEyes + case "😎": + title = strings.Gif_Emotion_Cool + case "🥳": + title = strings.Gif_Emotion_Party + default: + break + } + + guard let title = title else { + continue + } + + availableGifSearchEmojies.append(EntityKeyboardComponent.GifSearchEmoji(emoji: reaction, file: file, title: title)) } } @@ -995,6 +1162,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { gifContent = nil } + if let gifContentValue = gifContent { + if gifContentValue.items.isEmpty { + gifContent = nil + } + } + let entityKeyboardSize = self.entityKeyboardView.update( transition: mappedTransition, component: AnyComponent(EntityKeyboardComponent( @@ -1340,7 +1513,9 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV }, openStickerSettings: { }, - openPremiumSection: { + addGroupAction: { _, _ in + }, + clearGroup: { _ in }, pushController: { _ in }, diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 692d82c6a2..209e9bede4 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -184,9 +184,9 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { component: AnyComponent(LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: !isEmoji ? "anim_stickertosmile" : "anim_smiletosticker", - colors: colors, mode: .animateTransitionFromPrevious ), + colors: colors, size: animationFrame.size )), environment: {},