From c51b58ca728ec6abca3413be381afdc9bc107238 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Wed, 13 Jul 2022 01:21:54 +0200 Subject: [PATCH 1/2] [WIP] Entity input panel --- .../Telegram-iOS/en.lproj/Localizable.strings | 6 + .../Sources/AccountContext.swift | 7 +- .../Sources/ChatListController.swift | 2 +- .../Sources/ChatListSearchContainerNode.swift | 2 +- .../Sources/Node/ChatListItem.swift | 3 +- .../Source/Base/Transition.swift | 140 +++- .../Source/Components/Button.swift | 4 +- .../Source/Host/ComponentHostView.swift | 3 - .../Sources/LottieAnimationComponent.swift | 21 +- .../Sources/PagerComponent.swift | 23 +- .../Sources/ContactsControllerNode.swift | 2 +- .../ContextUI/Sources/ContextController.swift | 30 +- .../ContainedViewLayoutTransition.swift | 4 +- .../Source/ContextContentSourceNode.swift | 6 +- .../Source/ContextControllerSourceNode.swift | 2 +- submodules/Display/Source/TextNode.swift | 9 +- .../Sources/ListMessageSnippetItemNode.swift | 6 + .../Sources/PeersNearbyController.swift | 2 +- .../ArchivedStickerPacksController.swift | 3 + .../InstalledStickerPacksController.swift | 77 +- .../Sources/ThemePickerController.swift | 2 +- .../Themes/ThemeSettingsController.swift | 2 +- .../StickerPackPreviewControllerNode.swift | 4 + .../Sources/StickerPackScreen.swift | 4 + .../Components/MediaStreamComponent.swift | 2 +- .../Sources/Account/Account.swift | 1 + .../Network/FetchedMediaResource.swift | 55 +- .../State/AccountStateManagementUtils.swift | 16 + ...onizeInstalledStickerPacksOperations.swift | 12 +- .../Sources/State/StickerManagement.swift | 1 + ...ronizeInstalledStickerPacksOperation.swift | 5 + .../SyncCore/SyncCore_MediaReference.swift | 29 + .../SyncCore/SyncCore_Namespaces.swift | 2 + .../SyncCore_StickerPackCollectionInfo.swift | 4 + ...onizeInstalledStickerPacksOperations.swift | 1 + .../SyncCore/SyncCore_TelegramMediaFile.swift | 34 + .../Peers/UpdateCachedPeerData.swift | 2 + .../Stickers/ArchivedStickerPacks.swift | 5 + .../Stickers/CachedStickerPack.swift | 2 +- .../Stickers/ImportStickers.swift | 2 + .../Stickers/LoadedStickerPack.swift | 2 + .../TelegramEngine/Stickers/StickerPack.swift | 3 + .../StickerPackInteractiveOperations.swift | 4 + .../Resources/PresentationResourceKey.swift | 1 + .../Resources/PresentationResourcesChat.swift | 6 + .../Sources/EmojiTextAttachmentView.swift | 32 +- .../Components/EntityKeyboard/BUILD | 2 + .../Sources/EmojiPagerContentComponent.swift | 714 ++++++++++++---- .../Sources/EntityKeyboard.swift | 161 +++- ...tyKeyboardTopContainerPanelComponent.swift | 19 +- .../EntityKeyboardTopPanelComponent.swift | 761 ++++++++++++++++- .../Sources/GifPagerContentComponent.swift | 53 +- .../Sources/LottieAnimationCache.swift | 6 + .../Components/MultiAnimationRenderer/BUILD | 43 + .../MultiAnimationRendererShaders.metal | 38 + .../Sources/MultiAnimationMetalRenderer.swift | 767 ++++++++++++++++++ .../Sources/MultiAnimationRenderer.swift | 45 +- .../Sources/TextNodeWithEntities.swift | 13 +- .../Animations/emojicat_activity.json | 1 + .../Animations/emojicat_animals.json | 1 + .../Resources/Animations/emojicat_flags.json | 1 + .../Resources/Animations/emojicat_food.json | 1 + .../Animations/emojicat_objects.json | 1 + .../Resources/Animations/emojicat_places.json | 1 + .../Resources/Animations/emojicat_smiles.json | 1 + .../Animations/emojicat_symbols.json | 1 + submodules/TelegramUI/Resources/emoji1016.txt | 15 + .../TelegramUI/Sources/ChatController.swift | 10 +- .../Sources/ChatControllerNode.swift | 8 +- .../Sources/ChatEntityKeyboardInputNode.swift | 416 ++++++++-- .../Sources/ChatMediaInputNode.swift | 10 +- .../Sources/ChatMessageActionItemNode.swift | 3 +- .../ChatMessageAnimatedStickerItemNode.swift | 6 +- .../Sources/ChatMessageBubbleItemNode.swift | 20 +- .../ChatMessageInstantVideoItemNode.swift | 6 +- .../Sources/ChatMessageReplyInfoNode.swift | 6 +- .../Sources/ChatMessageStickerItemNode.swift | 6 +- .../ChatMessageTextBubbleContentNode.swift | 8 +- .../ChatPinnedMessageTitlePanelNode.swift | 3 +- .../ChatRecentActionsControllerNode.swift | 3 +- .../ChatSearchResultsContollerNode.swift | 2 +- .../Sources/ChatTextInputPanelNode.swift | 5 + .../Sources/FeaturedStickersScreen.swift | 8 +- ...rizontalStickersChatContextPanelNode.swift | 4 +- .../Sources/InlineReactionSearchPanel.swift | 4 +- .../TelegramUI/Sources/OpenResolvedUrl.swift | 2 +- submodules/TelegramUI/Sources/OpenUrl.swift | 16 + .../Sources/PeerInfo/PeerInfoScreen.swift | 2 +- .../Sources/ReplyAccessoryPanelNode.swift | 3 +- .../StickersChatInputContextPanelNode.swift | 4 +- .../TelegramUI/Sources/TextLinkHandling.swift | 8 +- .../Sources/UndoOverlayController.swift | 2 +- .../Sources/UndoOverlayControllerNode.swift | 13 +- .../UrlHandling/Sources/UrlHandling.swift | 14 +- .../MetalWallpaperBackgroundNode.swift | 1 + .../CoreAnimation/CoreAnimationLayer.swift | 4 + .../MainThreadAnimationLayer.swift | 8 + .../Sources/Private/RootAnimationLayer.swift | 1 + .../AnimationKeypathExtension.swift | 23 + .../Public/Animation/AnimationView.swift | 4 + .../DynamicProperties/AnimationKeypath.swift | 2 +- 101 files changed, 3374 insertions(+), 491 deletions(-) create mode 100644 submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal create mode 100644 submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift create mode 100644 submodules/TelegramUI/Resources/Animations/emojicat_activity.json create mode 100644 submodules/TelegramUI/Resources/Animations/emojicat_animals.json create mode 100644 submodules/TelegramUI/Resources/Animations/emojicat_flags.json create mode 100644 submodules/TelegramUI/Resources/Animations/emojicat_food.json create mode 100644 submodules/TelegramUI/Resources/Animations/emojicat_objects.json create mode 100644 submodules/TelegramUI/Resources/Animations/emojicat_places.json create mode 100644 submodules/TelegramUI/Resources/Animations/emojicat_smiles.json create mode 100644 submodules/TelegramUI/Resources/Animations/emojicat_symbols.json create mode 100644 submodules/TelegramUI/Resources/emoji1016.txt diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index f9142b2715..367d872258 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7834,3 +7834,9 @@ Sorry for the inconvenience."; "Notification.PremiumGift.Title" = "Telegram Premium"; "Notification.PremiumGift.Subtitle" = "for %@"; "Notification.PremiumGift.View" = "View"; + +"StickerPack.AddEmojiCount_1" = "Add 1 Emoji"; +"StickerPack.AddEmojiCount_any" = "Add %@ Emoji"; + +"StickerPack.RemoveEmojiCount_1" = "Remove 1 Emoji"; +"StickerPack.RemoveEmojiCount_any" = "Remove %@ Emoji"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index f19aa57c43..aa44f81472 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -260,6 +260,11 @@ public struct ResolvedBotAdminRights: OptionSet { } } +public enum StickerPackUrlType { + case stickers + case emoji +} + public enum ResolvedUrl { case externalUrl(String) case urlAuth(String) @@ -269,7 +274,7 @@ public enum ResolvedUrl { case groupBotStart(peerId: PeerId, payload: String, adminRights: ResolvedBotAdminRights?) case channelMessage(peerId: PeerId, messageId: MessageId, timecode: Double?) case replyThreadMessage(replyThreadMessage: ChatReplyThreadMessage, messageId: MessageId) - case stickerPack(name: String) + case stickerPack(name: String, type: StickerPackUrlType) case instantView(TelegramMediaWebpage, String?) case proxy(host: String, port: Int32, username: String?, password: String?, secret: Data?) case join(String) diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 8cd265b4c0..539cf35828 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -91,7 +91,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent let sourceNode = self.sourceNode return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in if let sourceNode = sourceNode { - return (sourceNode, sourceNode.bounds) + return (sourceNode.view, sourceNode.bounds) } else { return nil } diff --git a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift index 36c6fe2a12..000eeaa071 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchContainerNode.swift @@ -1412,7 +1412,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent let sourceNode = self.sourceNode return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in if let sourceNode = sourceNode { - return (sourceNode, sourceNode.bounds) + return (sourceNode.view, sourceNode.bounds) } else { return nil } diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 6a797147fa..ab5dcb5491 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1801,7 +1801,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { context: item.context, cache: item.interaction.animationCache, renderer: item.interaction.animationRenderer, - placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor + placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, + attemptSynchronous: synchronousLoads )) let _ = authorApply() diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 363a67f497..c21d1afcf7 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -53,7 +53,7 @@ private func makeSpringAnimation(keyPath: String) -> CASpringAnimation { } private extension CALayer { - func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) -> CAAnimation { + func makeAnimation(from: AnyObject?, to: AnyObject, keyPath: String, duration: Double, delay: Double, curve: Transition.Animation.Curve, removeOnCompletion: Bool, additive: Bool, completion: ((Bool) -> Void)? = nil) -> CAAnimation { switch curve { case .spring: let animation = makeSpringAnimation(keyPath: keyPath) @@ -88,12 +88,14 @@ private extension CALayer { } let animation = CABasicAnimation(keyPath: keyPath) - animation.fromValue = from + if let from = from { + animation.fromValue = from + } animation.toValue = to animation.duration = duration animation.timingFunction = curve.asTimingFunction() animation.isRemovedOnCompletion = removeOnCompletion - animation.fillMode = .forwards + animation.fillMode = .both animation.speed = speed animation.isAdditive = additive if let completion = completion { @@ -225,6 +227,36 @@ public struct Transition { } } + public func setFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) { + if layer.frame == frame { + completion?(true) + return + } + switch self.animation { + case .none: + layer.frame = frame + //view.bounds = CGRect(origin: view.bounds.origin, size: frame.size) + //view.layer.position = CGPoint(x: frame.midX, y: frame.midY) + layer.removeAnimation(forKey: "position") + layer.removeAnimation(forKey: "bounds") + completion?(true) + case .curve: + let previousFrame: CGRect + if (layer.animation(forKey: "position") != nil || layer.animation(forKey: "bounds") != nil), let presentation = layer.presentation() { + previousFrame = presentation.frame + } else { + previousFrame = layer.frame + } + + layer.frame = frame + //view.bounds = CGRect(origin: previousBounds.origin, size: frame.size) + //view.center = CGPoint(x: frame.midX, y: frame.midY) + + self.animatePosition(layer: layer, from: CGPoint(x: previousFrame.midX, y: previousFrame.midY), to: CGPoint(x: frame.midX, y: frame.midY), completion: completion) + self.animateBounds(layer: layer, from: CGRect(origin: layer.bounds.origin, size: previousFrame.size), to: CGRect(origin: layer.bounds.origin, size: frame.size)) + } + } + public func setBounds(view: UIView, bounds: CGRect, completion: ((Bool) -> Void)? = nil) { if view.bounds == bounds { completion?(true) @@ -454,6 +486,10 @@ public struct Transition { self.animateBounds(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion) } + public func animateBoundsOrigin(view: UIView, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animateBoundsOrigin(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion) + } + public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: @@ -491,4 +527,102 @@ public struct Transition { ) } } + + public func animateBoundsOrigin(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + break + case let .curve(duration, curve): + layer.animate( + from: NSValue(cgPoint: fromValue), + to: NSValue(cgPoint: toValue), + keyPath: "bounds.origin", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: additive, + completion: completion + ) + } + } + + public func setCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) { + if layer.cornerRadius == cornerRadius { + return + } + switch self.animation { + case .none: + layer.cornerRadius = cornerRadius + completion?(true) + case let .curve(duration, curve): + let fromValue: CGFloat + if layer.animation(forKey: "cornerRadius") != nil, let presentation = layer.presentation() { + fromValue = presentation.cornerRadius + } else { + fromValue = layer.cornerRadius + } + layer.cornerRadius = cornerRadius + layer.animate( + from: fromValue as NSNumber, + to: cornerRadius as NSNumber, + keyPath: "cornerRadius", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } + } + + public func setShapeLayerPath(layer: CAShapeLayer, path: CGPath, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + layer.path = path + case let .curve(duration, curve): + if let previousPath = layer.path { + layer.animate( + from: previousPath, + to: path, + keyPath: "path", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + layer.path = path + } else { + layer.path = path + } + } + } + + public func setShapeLayerLineDashPattern(layer: CAShapeLayer, pattern: [NSNumber], completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + layer.lineDashPattern = pattern + case let .curve(duration, curve): + if let previousLineDashPattern = layer.lineDashPattern { + layer.lineDashPattern = pattern + + layer.animate( + from: previousLineDashPattern as CFArray, + to: pattern as CFArray, + keyPath: "lineDashPattern", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: false, + completion: completion + ) + } else { + layer.lineDashPattern = pattern + } + } + } } diff --git a/submodules/ComponentFlow/Source/Components/Button.swift b/submodules/ComponentFlow/Source/Components/Button.swift index 27f749aa5f..1332bd0af0 100644 --- a/submodules/ComponentFlow/Source/Components/Button.swift +++ b/submodules/ComponentFlow/Source/Components/Button.swift @@ -154,7 +154,7 @@ public final class Button: Component { self.holdActionTimer?.invalidate() if #available(iOS 10.0, *) { - let holdActionTimer = Timer(timeInterval: 1.0, repeats: false, block: { [weak self] _ in + let holdActionTimer = Timer(timeInterval: 0.5, repeats: false, block: { [weak self] _ in guard let strongSelf = self else { return } @@ -173,7 +173,7 @@ public final class Button: Component { private func beginExecuteHoldActionTimer() { self.holdActionTimer?.invalidate() if #available(iOS 10.0, *) { - let holdActionTimer = Timer(timeInterval: 0.2, repeats: true, block: { [weak self] _ in + let holdActionTimer = Timer(timeInterval: 0.1, repeats: true, block: { [weak self] _ in guard let strongSelf = self else { return } diff --git a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift index e5407341f4..ec1ae334f0 100644 --- a/submodules/ComponentFlow/Source/Host/ComponentHostView.swift +++ b/submodules/ComponentFlow/Source/Host/ComponentHostView.swift @@ -203,9 +203,6 @@ public final class ComponentView { } let updatedSize = component._update(view: componentView, availableSize: containerSize, environment: context.erasedEnvironment, transition: transition) - if transition.userData(ComponentHostViewSkipSettingFrame.self) == nil { - transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(), size: updatedSize)) - } if isEnvironmentUpdated { context.erasedEnvironment._isUpdated = false diff --git a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift index cad54a967a..8c773c16df 100644 --- a/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift +++ b/submodules/Components/LottieAnimationComponent/Sources/LottieAnimationComponent.swift @@ -7,8 +7,13 @@ import Display public final class LottieAnimationComponent: Component { public struct AnimationItem: Equatable { + public enum StillPosition { + case begin + case end + } + public enum Mode: Equatable { - case still + case still(position: StillPosition) case animating(loop: Bool) case animateTransitionFromPrevious } @@ -153,7 +158,11 @@ public final class LottieAnimationComponent: Component { view.backgroundColor = .clear view.isOpaque = false - //view.logHierarchyKeypaths() + 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")) @@ -188,6 +197,14 @@ public final class LottieAnimationComponent: Component { } } } else { + if case let .still(position) = component.animation.mode { + switch position { + case .begin: + animationView.currentFrame = 0.0 + case .end: + animationView.currentFrame = animationView.animation?.endFrame ?? 0.0 + } + } if animationView.isAnimationPlaying { animationView.stop() } diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index a122249858..aa06aec2fa 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -298,6 +298,8 @@ public final class PagerComponent (ASDisplayNode, CGRect)? + public let sourceNode: () -> (UIView, CGRect)? - public init(contentAreaInScreenSpace: CGRect, sourceNode: @escaping () -> (ASDisplayNode, CGRect)?) { + public init(contentAreaInScreenSpace: CGRect, sourceNode: @escaping () -> (UIView, CGRect)?) { self.contentAreaInScreenSpace = contentAreaInScreenSpace self.sourceNode = sourceNode } diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 15fea6c105..32da03d32b 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -625,8 +625,8 @@ public extension ContainedViewLayoutTransition { } else { switch self { case .immediate: - view.layer.removeAnimation(forKey: "position") - view.layer.removeAnimation(forKey: "bounds") + //view.layer.removeAnimation(forKey: "position") + //view.layer.removeAnimation(forKey: "bounds") view.frame = frame if let completion = completion { completion(true) diff --git a/submodules/Display/Source/ContextContentSourceNode.swift b/submodules/Display/Source/ContextContentSourceNode.swift index 8e44c9e681..b4ed6a4cbd 100644 --- a/submodules/Display/Source/ContextContentSourceNode.swift +++ b/submodules/Display/Source/ContextContentSourceNode.swift @@ -115,12 +115,12 @@ public final class ContextExtractedContentView: UIView { } public final class ContextControllerContentNode: ASDisplayNode { - public let sourceNode: ASDisplayNode + public let sourceView: UIView public let controller: ViewController private let tapped: () -> Void - public init(sourceNode: ASDisplayNode, controller: ViewController, tapped: @escaping () -> Void) { - self.sourceNode = sourceNode + public init(sourceView: UIView, controller: ViewController, tapped: @escaping () -> Void) { + self.sourceView = sourceView self.controller = controller self.tapped = tapped diff --git a/submodules/Display/Source/ContextControllerSourceNode.swift b/submodules/Display/Source/ContextControllerSourceNode.swift index 3e2d8916c5..0bfd0ce673 100644 --- a/submodules/Display/Source/ContextControllerSourceNode.swift +++ b/submodules/Display/Source/ContextControllerSourceNode.swift @@ -271,7 +271,7 @@ open class ContextControllerSourceView: UIView { public weak var additionalActivationProgressLayer: CALayer? public var targetNodeForActivationProgress: ASDisplayNode? public var targetViewForActivationProgress: UIView? - public var targetLayerForActivationProgress: CALayer? + public weak var targetLayerForActivationProgress: CALayer? public var targetNodeForActivationProgressContentRect: CGRect? public var useSublayerTransformForActivation: Bool = true diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 8231bad342..d77115e99d 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1126,6 +1126,10 @@ open class TextNode: ASDisplayNode { rightOffset = ceil(secondaryRightOffset) } + if embeddedItems.count > 25 { + assert(true) + } + embeddedItems.append(TextNodeEmbeddedItem(range: NSMakeRange(startIndex, endIndex - startIndex + 1), frame: CGRect(x: min(leftOffset, rightOffset), y: descent - (ascent + descent), width: abs(rightOffset - leftOffset) + rightInset, height: ascent + descent), item: item)) } @@ -1136,6 +1140,9 @@ open class TextNode: ASDisplayNode { isLastLine = true } if isLastLine { + if attributedString.string.hasPrefix("😀") { + assert(true) + } if first { first = false } else { @@ -1175,7 +1182,7 @@ open class TextNode: ASDisplayNode { for run in runs { let runAttributes: NSDictionary = CTRunGetAttributes(run) if let _ = runAttributes["CTForegroundColorFromContext"] { - brokenLineRange.length = CTRunGetStringRange(run).location + brokenLineRange.length = CTRunGetStringRange(run).location - brokenLineRange.location break } } diff --git a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift index e5aecb7f92..131e2b51fb 100644 --- a/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift +++ b/submodules/ListMessageItem/Sources/ListMessageSnippetItemNode.swift @@ -386,6 +386,10 @@ public final class ListMessageSnippetItemNode: ListMessageNode { title = NSAttributedString(string: urlString, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor) iconText = NSAttributedString(string: "S", font: iconFont, textColor: UIColor.white) + } else if url.path.hasPrefix("/addemoji/") { + title = NSAttributedString(string: urlString, font: titleFont, textColor: item.presentationData.theme.theme.list.itemPrimaryTextColor) + + iconText = NSAttributedString(string: "E", font: iconFont, textColor: UIColor.white) } else { iconText = NSAttributedString(string: host[.. Void let openStickersBot: () -> Void let openMasks: () -> Void + let openEmoji: () -> Void let openQuickReaction: () -> Void let openFeatured: () -> Void let openArchived: ([ArchivedStickerPackItem]?) -> Void @@ -35,13 +36,14 @@ private final class InstalledStickerPacksControllerArguments { let expandTrendingPacks: () -> Void let addPack: (StickerPackCollectionInfo) -> Void - init(account: Account, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, removePack: @escaping (ArchivedStickerPackItem) -> Void, openStickersBot: @escaping () -> Void, openMasks: @escaping () -> Void, openQuickReaction: @escaping () -> Void, openFeatured: @escaping () -> Void, openArchived: @escaping ([ArchivedStickerPackItem]?) -> Void, openSuggestOptions: @escaping () -> Void, toggleAnimatedStickers: @escaping (Bool) -> Void, togglePackSelected: @escaping (ItemCollectionId) -> Void, expandTrendingPacks: @escaping () -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) { + init(account: Account, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, setPackIdWithRevealedOptions: @escaping (ItemCollectionId?, ItemCollectionId?) -> Void, removePack: @escaping (ArchivedStickerPackItem) -> Void, openStickersBot: @escaping () -> Void, openMasks: @escaping () -> Void, openEmoji: @escaping () -> Void, openQuickReaction: @escaping () -> Void, openFeatured: @escaping () -> Void, openArchived: @escaping ([ArchivedStickerPackItem]?) -> Void, openSuggestOptions: @escaping () -> Void, toggleAnimatedStickers: @escaping (Bool) -> Void, togglePackSelected: @escaping (ItemCollectionId) -> Void, expandTrendingPacks: @escaping () -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) { self.account = account self.openStickerPack = openStickerPack self.setPackIdWithRevealedOptions = setPackIdWithRevealedOptions self.removePack = removePack self.openStickersBot = openStickersBot self.openMasks = openMasks + self.openEmoji = openEmoji self.openQuickReaction = openQuickReaction self.openFeatured = openFeatured self.openArchived = openArchived @@ -83,6 +85,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { case trending(PresentationTheme, String, Int32) case archived(PresentationTheme, String, Int32, [ArchivedStickerPackItem]?) case masks(PresentationTheme, String) + case emoji(PresentationTheme, String) case quickReaction(String, UIImage?) case animatedStickers(PresentationTheme, String, Bool) case animatedStickersInfo(PresentationTheme, String) @@ -95,7 +98,7 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { var section: ItemListSectionId { switch self { - case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo: + case .suggestOptions, .trending, .masks, .emoji, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo: return InstalledStickerPacksSection.service.rawValue case .trendingPacksTitle, .trendingPack, .trendingExpand: return InstalledStickerPacksSection.trending.rawValue @@ -114,24 +117,26 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { return .index(2) case .masks: return .index(3) - case .quickReaction: + case .emoji: return .index(4) - case .animatedStickers: + case .quickReaction: return .index(5) - case .animatedStickersInfo: + case .animatedStickers: return .index(6) - case .trendingPacksTitle: + case .animatedStickersInfo: return .index(7) + case .trendingPacksTitle: + return .index(8) case let .trendingPack(_, _, _, info, _, _, _, _, _): return .trendingPack(info.id) case .trendingExpand: - return .index(8) - case .packsTitle: return .index(9) + case .packsTitle: + return .index(10) case let .pack(_, _, _, info, _, _, _, _, _, _): return .pack(info.id) case .packsInfo: - return .index(10) + return .index(11) } } @@ -155,6 +160,12 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { } else { return false } + case let .emoji(lhsTheme, lhsCount): + if case let .emoji(rhsTheme, rhsCount) = rhs, lhsTheme === rhsTheme, lhsCount == rhsCount { + return true + } else { + return false + } case let .quickReaction(lhsText, lhsImage): if case let .quickReaction(rhsText, rhsImage) = rhs, lhsText == rhsText, lhsImage === rhsImage { return true @@ -305,30 +316,37 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { default: return true } + case .emoji: + switch rhs { + case .suggestOptions, .trending, .archived, .masks, .emoji: + return false + default: + return true + } case .quickReaction: switch rhs { - case .suggestOptions, .trending, .archived, .masks, .quickReaction: + case .suggestOptions, .trending, .archived, .masks, .emoji, .quickReaction: return false default: return true } case .animatedStickers: switch rhs { - case .suggestOptions, .trending, .archived, .masks, .quickReaction, .animatedStickers: + case .suggestOptions, .trending, .archived, .masks, .emoji, .quickReaction, .animatedStickers: return false default: return true } case .animatedStickersInfo: switch rhs { - case .suggestOptions, .trending, .archived, .masks, .quickReaction, .animatedStickers, .animatedStickersInfo: + case .suggestOptions, .trending, .archived, .masks, .emoji, .quickReaction, .animatedStickers, .animatedStickersInfo: return false default: return true } case .trendingPacksTitle: switch rhs { - case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle: + case .suggestOptions, .trending, .masks, .emoji, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle: return false default: return true @@ -344,14 +362,14 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { } case .trendingExpand: switch rhs { - case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand: + case .suggestOptions, .trending, .masks, .emoji, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand: return false default: return true } case .packsTitle: switch rhs { - case .suggestOptions, .trending, .masks, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand, .packsTitle: + case .suggestOptions, .trending, .masks, .emoji, .quickReaction, .archived, .animatedStickers, .animatedStickersInfo, .trendingPacksTitle, .trendingPack, .trendingExpand, .packsTitle: return false default: return true @@ -390,6 +408,10 @@ private indirect enum InstalledStickerPacksEntry: ItemListNodeEntry { return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { arguments.openMasks() }) + case let .emoji(_, text): + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: "", sectionId: self.section, style: .blocks, action: { + arguments.openEmoji() + }) case let .quickReaction(title, image): let labelStyle: ItemListDisclosureLabelStyle if let image = image { @@ -505,6 +527,8 @@ private func namespaceForMode(_ mode: InstalledStickerPacksControllerMode) -> It return Namespaces.ItemCollection.CloudStickerPacks case .masks: return Namespaces.ItemCollection.CloudMaskPacks + case .emoji: + return Namespaces.ItemCollection.CloudEmojiPacks } } @@ -544,6 +568,9 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati } entries.append(.masks(presentationData.theme, presentationData.strings.MaskStickerSettings_Title)) + //TODO:localize + entries.append(.emoji(presentationData.theme, "Emoji")) + entries.append(.quickReaction(presentationData.strings.Settings_QuickReactionSetup_NavigationTitle, quickReactionImage)) entries.append(.animatedStickers(presentationData.theme, presentationData.strings.StickerPacksSettings_AnimatedStickers, stickerSettings.loopAnimatedStickers)) @@ -576,6 +603,11 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati if let archived = archived, !archived.isEmpty { entries.append(.archived(presentationData.theme, presentationData.strings.StickerPacksSettings_ArchivedMasks, Int32(archived.count), archived)) } + case .emoji: + if let archived = archived, !archived.isEmpty { + //TODO:localize + entries.append(.archived(presentationData.theme, "Archived Emoji", Int32(archived.count), archived)) + } } if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [namespaceForMode(mode)])] as? ItemCollectionInfosView { @@ -618,6 +650,9 @@ private func installedStickerPacksControllerEntries(presentationData: Presentati markdownString = presentationData.strings.StickerPacksSettings_ManagingHelp case .masks: markdownString = presentationData.strings.MaskStickerSettings_Info + case .emoji: + //TODO:localize + markdownString = "Emoji" } let entities = generateTextEntities(markdownString, enabledTypes: [.mention]) if let entity = entities.first { @@ -633,6 +668,7 @@ public enum InstalledStickerPacksControllerMode { case general case modal case masks + case emoji } public func installedStickerPacksController(context: AccountContext, mode: InstalledStickerPacksControllerMode, archivedPacks: [ArchivedStickerPackItem]? = nil, updatedPacks: @escaping ([ArchivedStickerPackItem]?) -> Void = { _ in }, focusOnItemTag: InstalledStickerPacksEntryTag? = nil) -> ViewController { @@ -731,6 +767,8 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta })) }, openMasks: { pushControllerImpl?(installedStickerPacksController(context: context, mode: .masks, archivedPacks: archivedPacks, updatedPacks: { _ in})) + }, openEmoji: { + pushControllerImpl?(installedStickerPacksController(context: context, mode: .emoji, archivedPacks: archivedPacks, updatedPacks: { _ in})) }, openQuickReaction: { pushControllerImpl?(quickReactionSetupController( context: context @@ -742,6 +780,8 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta switch mode { case .masks: archivedMode = .masks + case .emoji: + archivedMode = .emoji default: archivedMode = .stickers } @@ -876,6 +916,10 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta featured.set(.single([])) archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .masks) |> map(Optional.init))) quickReactionImage = .single(nil) + case .emoji: + featured.set(.single([])) + archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks(namespace: .emoji) |> map(Optional.init))) + quickReactionImage = .single(nil) } var previousPackCount: Int? @@ -1027,6 +1071,9 @@ public func installedStickerPacksController(context: AccountContext, mode: Insta title = presentationData.strings.StickerPacksSettings_Title case .masks: title = presentationData.strings.MaskStickerSettings_Title + case .emoji: + //TODO:localize + title = "Emoji" } let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) diff --git a/submodules/SettingsUI/Sources/ThemePickerController.swift b/submodules/SettingsUI/Sources/ThemePickerController.swift index f02b48e1eb..a620c20fa7 100644 --- a/submodules/SettingsUI/Sources/ThemePickerController.swift +++ b/submodules/SettingsUI/Sources/ThemePickerController.swift @@ -1259,7 +1259,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent let sourceNode = self.sourceNode return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in if let sourceNode = sourceNode { - return (sourceNode, sourceNode.bounds) + return (sourceNode.view, sourceNode.bounds) } else { return nil } diff --git a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift index c5f815fe6e..137d752d52 100644 --- a/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift +++ b/submodules/SettingsUI/Sources/Themes/ThemeSettingsController.swift @@ -1405,7 +1405,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent let sourceNode = self.sourceNode return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in if let sourceNode = sourceNode { - return (sourceNode, sourceNode.bounds) + return (sourceNode.view, sourceNode.bounds) } else { return nil } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift index d17a2d11ba..8aefdfae14 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift @@ -620,6 +620,8 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol let text: String if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { text = self.presentationData.strings.StickerPack_RemoveStickerCount(info.count) + } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { + text = self.presentationData.strings.StickerPack_RemoveEmojiCount(info.count) } else { text = self.presentationData.strings.StickerPack_RemoveMaskCount(info.count) } @@ -628,6 +630,8 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol let text: String if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { text = self.presentationData.strings.StickerPack_AddStickerCount(info.count) + } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { + text = self.presentationData.strings.StickerPack_AddEmojiCount(info.count) } else { text = self.presentationData.strings.StickerPack_AddMaskCount(info.count) } diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 8ce32224e7..7a8e8ce58d 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -701,6 +701,8 @@ private final class StickerPackContainer: ASDisplayNode { let text: String if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { text = self.presentationData.strings.StickerPack_RemoveStickerCount(Int32(entries.count)) + } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { + text = self.presentationData.strings.StickerPack_RemoveEmojiCount(Int32(entries.count)) } else { text = self.presentationData.strings.StickerPack_RemoveMaskCount(Int32(entries.count)) } @@ -710,6 +712,8 @@ private final class StickerPackContainer: ASDisplayNode { let text: String if info.id.namespace == Namespaces.ItemCollection.CloudStickerPacks { text = self.presentationData.strings.StickerPack_AddStickerCount(Int32(entries.count)) + } else if info.id.namespace == Namespaces.ItemCollection.CloudEmojiPacks { + text = self.presentationData.strings.StickerPack_AddEmojiCount(Int32(entries.count)) } else { text = self.presentationData.strings.StickerPack_AddMaskCount(Int32(entries.count)) } diff --git a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift index 930de2caa4..399eda693e 100644 --- a/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift +++ b/submodules/TelegramCallsUI/Sources/Components/MediaStreamComponent.swift @@ -805,7 +805,7 @@ public final class MediaStreamComponent: CombinedComponent { "Point 3.Group 1.Fill 1": whiteColor, "Point 1.Group 1.Fill 1": whiteColor ], - mode: .still + mode: .still(position: .begin) ), size: CGSize(width: 22.0, height: 22.0) ).tagged(moreAnimationTag))), diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 39dc7502c4..7d21c9bddd 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1078,6 +1078,7 @@ public class Account { self.managedOperationsDisposable.add(managedSynchronizeGroupedPeersOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager, namespace: .stickers).start()) self.managedOperationsDisposable.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager, namespace: .masks).start()) + self.managedOperationsDisposable.add(managedSynchronizeInstalledStickerPacksOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager, namespace: .emoji).start()) self.managedOperationsDisposable.add(managedSynchronizeMarkFeaturedStickerPacksAsSeenOperations(postbox: self.postbox, network: self.network).start()) self.managedOperationsDisposable.add(managedSynchronizeRecentlyUsedMediaOperations(postbox: self.postbox, network: self.network, category: .stickers, revalidationContext: self.mediaReferenceRevalidationContext).start()) self.managedOperationsDisposable.add(managedSynchronizeSavedGifsOperations(postbox: self.postbox, network: self.network, revalidationContext: self.mediaReferenceRevalidationContext).start()) diff --git a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift index aa1e63dd32..d29e251724 100644 --- a/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift +++ b/submodules/TelegramCore/Sources/Network/FetchedMediaResource.swift @@ -1,7 +1,7 @@ import Foundation import Postbox import SwiftSignalKit - +import TelegramApi extension MediaResourceReference { var apiFileReference: Data? { @@ -227,6 +227,7 @@ private enum MediaReferenceRevalidationKey: Hashable { case peerAvatars(peer: PeerReference) case attachBot(peer: PeerReference) case notificationSoundList + case customEmoji(fileId: Int64) } private final class MediaReferenceRevalidationItemContext { @@ -391,6 +392,26 @@ final class MediaReferenceRevalidationContext { } } + func customEmoji(postbox: Postbox, network: Network, background: Bool, fileId: Int64) -> Signal { + return network.request(Api.functions.messages.getCustomEmojiDocuments(documentId: [fileId])) + |> map(Optional.init) + |> `catch` { _ -> Signal<[Api.Document]?, NoError> in + return .single(nil) + } + |> castError(RevalidateMediaReferenceError.self) + |> mapToSignal { result -> Signal in + guard let result = result else { + return .fail(.generic) + } + for document in result { + if let file = telegramMediaFileFromApiDocument(document) { + return .single(file) + } + } + return .fail(.generic) + } + } + func webPage(postbox: Postbox, network: Network, background: Bool, webPage: WebpageReference) -> Signal { return self.genericItem(key: .webPage(webPage: webPage), background: background, request: { next, error in return (updatedRemoteWebpage(postbox: postbox, network: network, webPage: webPage) @@ -726,7 +747,37 @@ func revalidateMediaResourceReference(postbox: Postbox, network: Network, revali } } return .fail(.generic) - } + case let .customEmoji(media): + if let file = media as? TelegramMediaFile { + return revalidationContext.customEmoji(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, fileId: file.fileId.id) + |> mapToSignal { result -> Signal in + if let updatedResource = findUpdatedMediaResource(media: result, previousMedia: media, resource: resource) { + return postbox.transaction { transaction -> RevalidatedMediaResource in + if let id = media.id { + var attributes = result.attributes + if !attributes.contains(where: { attribute in + if case .hintIsValidated = attribute { + return true + } else { + return false + } + }) { + attributes.append(.hintIsValidated) + } + let file = result.withUpdatedAttributes(attributes) + updateMessageMedia(transaction: transaction, id: id, media: file) + } + return RevalidatedMediaResource(updatedResource: updatedResource, updatedReference: nil) + } + |> castError(RevalidateMediaReferenceError.self) + } else { + return .fail(.generic) + } + } + } else { + return .fail(.generic) + } + } case let .avatar(peer, _): return revalidationContext.peer(postbox: postbox, network: network, background: info.preferBackgroundReferenceRevalidation, peer: peer) |> mapToSignal { updatedPeer -> Signal in diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 73fa894fba..cd37a30e9d 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -1379,6 +1379,8 @@ private func finalStateWithUpdatesAndServerTime(postbox: Postbox, network: Netwo let namespace: SynchronizeInstalledStickerPacksOperationNamespace if (flags & (1 << 0)) != 0 { namespace = .masks + } else if (flags & (1 << 1)) != 0 { + namespace = .emoji } else { namespace = .stickers } @@ -3459,9 +3461,11 @@ func replayFinalState( }) { addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .stickers, content: .sync, noDelay: false) addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .masks, content: .sync, noDelay: false) + addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .emoji, content: .sync, noDelay: false) } else { var syncStickers = false var syncMasks = false + var syncEmoji = false loop: for operation in stickerPackOperations { switch operation { case let .add(apiSet): @@ -3501,6 +3505,8 @@ func replayFinalState( case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _): if (flags & (1 << 3)) != 0 { namespace = Namespaces.ItemCollection.CloudMaskPacks + } else if (flags & (1 << 7)) != 0 { + namespace = Namespaces.ItemCollection.CloudEmojiPacks } else { namespace = Namespaces.ItemCollection.CloudStickerPacks } @@ -3512,6 +3518,8 @@ func replayFinalState( continue loop } else if namespace == Namespaces.ItemCollection.CloudStickerPacks && syncStickers { continue loop + } else if namespace == Namespaces.ItemCollection.CloudEmojiPacks && syncEmoji { + continue loop } var updatedInfos = transaction.getItemCollectionsInfos(namespace: info.id.namespace).map { $0.1 as! StickerPackCollectionInfo } @@ -3532,6 +3540,8 @@ func replayFinalState( collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks case .masks: collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks + case .emoji: + collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks } let currentInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo } if Set(currentInfos.map { $0.id.id }) != Set(ids) { @@ -3540,6 +3550,8 @@ func replayFinalState( syncStickers = true case .masks: syncMasks = true + case .emoji: + syncEmoji = true } } else { var currentDict: [ItemCollectionId: StickerPackCollectionInfo] = [:] @@ -3556,6 +3568,7 @@ func replayFinalState( case .sync: syncStickers = true syncMasks = true + syncEmoji = true break loop } } @@ -3565,6 +3578,9 @@ func replayFinalState( if syncMasks { addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .masks, content: .sync, noDelay: false) } + if syncEmoji { + addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .emoji, content: .sync, noDelay: false) + } } } diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift index 1b73b655f3..bd5ca0c408 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizeInstalledStickerPacksOperations.swift @@ -72,6 +72,8 @@ func managedSynchronizeInstalledStickerPacksOperations(postbox: Postbox, network tag = OperationLogTags.SynchronizeInstalledStickerPacks case .masks: tag = OperationLogTags.SynchronizeInstalledMasks + case .emoji: + tag = OperationLogTags.SynchronizeInstalledEmoji } let helper = Atomic(value: ManagedSynchronizeInstalledStickerPacksOperationsHelper()) @@ -126,7 +128,7 @@ private func hashForStickerPackInfos(_ infos: [StickerPackCollectionInfo]) -> In var acc: UInt64 = 0 for info in infos { - combineInt64Hash(&acc, with: UInt64(UInt32(bitPattern: info.hash))) + combineInt64Hash(&acc, with: UInt64(bitPattern: Int64(info.hash))) } return finalizeInt64Hash(acc) @@ -295,6 +297,8 @@ private func reorderRemoteStickerPacks(network: Network, namespace: SynchronizeI break case .masks: flags |= (1 << 0) + case .emoji: + flags |= (1 << 1) } return network.request(Api.functions.messages.reorderStickerSets(flags: flags, order: ids.map { $0.id })) |> `catch` { _ -> Signal in @@ -312,6 +316,8 @@ private func synchronizeInstalledStickerPacks(transaction: Transaction, postbox: collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks case .masks: collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks + case .emoji: + collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks } let localCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo } @@ -435,6 +441,8 @@ private func continueSynchronizeInstalledStickerPacks(transaction: Transaction, collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks case .masks: collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks + case .emoji: + collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks } let localCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo } @@ -446,6 +454,8 @@ private func continueSynchronizeInstalledStickerPacks(transaction: Transaction, request = network.request(Api.functions.messages.getAllStickers(hash: initialLocalHash)) case .masks: request = network.request(Api.functions.messages.getMaskStickers(hash: initialLocalHash)) + case .emoji: + request = network.request(Api.functions.messages.getEmojiStickers(hash: initialLocalHash)) } let sequence = request diff --git a/submodules/TelegramCore/Sources/State/StickerManagement.swift b/submodules/TelegramCore/Sources/State/StickerManagement.swift index 3b17917a19..6ed01b4b11 100644 --- a/submodules/TelegramCore/Sources/State/StickerManagement.swift +++ b/submodules/TelegramCore/Sources/State/StickerManagement.swift @@ -17,6 +17,7 @@ func manageStickerPacks(network: Network, postbox: Postbox) -> Signal Void in addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .stickers, content: .sync, noDelay: false) addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .masks, content: .sync, noDelay: false) + addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: .emoji, content: .sync, noDelay: false) addSynchronizeSavedGifsOperation(transaction: transaction, operation: .sync) addSynchronizeSavedStickersOperation(transaction: transaction, operation: .sync) addSynchronizeRecentlyUsedMediaOperation(transaction: transaction, category: .stickers, operation: .sync) diff --git a/submodules/TelegramCore/Sources/State/SynchronizeInstalledStickerPacksOperation.swift b/submodules/TelegramCore/Sources/State/SynchronizeInstalledStickerPacksOperation.swift index 30939ae228..72656ed868 100644 --- a/submodules/TelegramCore/Sources/State/SynchronizeInstalledStickerPacksOperation.swift +++ b/submodules/TelegramCore/Sources/State/SynchronizeInstalledStickerPacksOperation.swift @@ -14,6 +14,8 @@ public func addSynchronizeInstalledStickerPacksOperation(transaction: Transactio operationNamespace = .stickers case Namespaces.ItemCollection.CloudMaskPacks: operationNamespace = .masks + case Namespaces.ItemCollection.CloudEmojiPacks: + operationNamespace = .emoji default: return } @@ -31,6 +33,9 @@ func addSynchronizeInstalledStickerPacksOperation(transaction: Transaction, name case .masks: tag = OperationLogTags.SynchronizeInstalledMasks itemCollectionNamespace = Namespaces.ItemCollection.CloudMaskPacks + case .emoji: + tag = OperationLogTags.SynchronizeInstalledEmoji + itemCollectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks } var previousStickerPackIds: [ItemCollectionId]? var archivedPacks: [ItemCollectionId] = [] diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift index 2516478eec..a1b029630c 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_MediaReference.swift @@ -225,6 +225,7 @@ public enum AnyMediaReference: Equatable { case savedGif(media: Media) case avatarList(peer: PeerReference, media: Media) case attachBot(peer: PeerReference, media: Media) + case customEmoji(media: Media) public static func ==(lhs: AnyMediaReference, rhs: AnyMediaReference) -> Bool { switch lhs { @@ -270,6 +271,12 @@ public enum AnyMediaReference: Equatable { } else { return false } + case let .customEmoji(lhsMedia): + if case let .customEmoji(rhsMedia) = rhs, lhsMedia.isEqual(to: rhsMedia) { + return true + } else { + return false + } } } @@ -289,6 +296,8 @@ public enum AnyMediaReference: Equatable { return nil case .attachBot: return nil + case .customEmoji: + return nil } } @@ -322,6 +331,10 @@ public enum AnyMediaReference: Equatable { if let media = media as? T { return .attachBot(peer: peer, media: media) } + case let .customEmoji(media): + if let media = media as? T { + return .customEmoji(media: media) + } } return nil } @@ -342,6 +355,8 @@ public enum AnyMediaReference: Equatable { return media case let .attachBot(_, media): return media + case let .customEmoji(media): + return media } } @@ -421,6 +436,7 @@ public enum MediaReference { case savedGif case avatarList case attachBot + case customEmoji } case standalone(media: T) @@ -430,6 +446,7 @@ public enum MediaReference { case savedGif(media: T) case avatarList(peer: PeerReference, media: T) case attachBot(peer: PeerReference, media: T) + case customEmoji(media: T) public init?(decoder: PostboxDecoder) { guard let caseIdValue = decoder.decodeOptionalInt32ForKey("_r"), let caseId = CodingCase(rawValue: caseIdValue) else { @@ -476,6 +493,11 @@ public enum MediaReference { return nil } self = .attachBot(peer: peer, media: media) + case .customEmoji: + guard let media = decoder.decodeObjectForKey("m") as? T else { + return nil + } + self = .customEmoji(media: media) } } @@ -507,6 +529,9 @@ public enum MediaReference { encoder.encodeInt32(CodingCase.attachBot.rawValue, forKey: "_r") encoder.encodeObject(peer, forKey: "pr") encoder.encodeObject(media, forKey: "m") + case let .customEmoji(media): + encoder.encodeInt32(CodingCase.customEmoji.rawValue, forKey: "_r") + encoder.encodeObject(media, forKey: "m") } } @@ -526,6 +551,8 @@ public enum MediaReference { return .avatarList(peer: peer, media: media) case let .attachBot(peer, media): return .attachBot(peer: peer, media: media) + case let .customEmoji(media): + return .customEmoji(media: media) } } @@ -549,6 +576,8 @@ public enum MediaReference { return media case let .attachBot(_, media): return media + case let .customEmoji(media): + return media } } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index db298b43d9..44b31cdbc4 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -46,6 +46,7 @@ public struct Namespaces { public static let CloudDice: Int32 = 4 public static let CloudAnimatedEmojiAnimations: Int32 = 5 public static let CloudAnimatedEmojiReactions: Int32 = 6 + public static let CloudEmojiPacks: Int32 = 8 } public struct OrderedItemList { @@ -164,6 +165,7 @@ public struct OperationLogTags { public static let SynchronizeEmojiKeywords = PeerOperationLogTag(value: 19) public static let SynchronizeChatListFilters = PeerOperationLogTag(value: 20) public static let SynchronizeMarkAllUnseenReactions = PeerOperationLogTag(value: 21) + public static let SynchronizeInstalledEmoji = PeerOperationLogTag(value: 22) } public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift index 599ebc1f36..bf6c9ad840 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_StickerPackCollectionInfo.swift @@ -27,6 +27,9 @@ public struct StickerPackCollectionInfoFlags: OptionSet { if flags.contains(StickerPackCollectionInfoFlags.isVideo) { rawValue |= StickerPackCollectionInfoFlags.isVideo.rawValue } + if flags.contains(StickerPackCollectionInfoFlags.isEmoji) { + rawValue |= StickerPackCollectionInfoFlags.isEmoji.rawValue + } self.rawValue = rawValue } @@ -35,6 +38,7 @@ public struct StickerPackCollectionInfoFlags: OptionSet { public static let isOfficial = StickerPackCollectionInfoFlags(rawValue: 1 << 1) public static let isAnimated = StickerPackCollectionInfoFlags(rawValue: 1 << 2) public static let isVideo = StickerPackCollectionInfoFlags(rawValue: 1 << 3) + public static let isEmoji = StickerPackCollectionInfoFlags(rawValue: 1 << 4) } diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeInstalledStickerPacksOperations.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeInstalledStickerPacksOperations.swift index c7ad51c754..03ff97549f 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeInstalledStickerPacksOperations.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_SynchronizeInstalledStickerPacksOperations.swift @@ -4,6 +4,7 @@ import Postbox public enum SynchronizeInstalledStickerPacksOperationNamespace: Int32 { case stickers = 0 case masks = 1 + case emoji } public final class SynchronizeInstalledStickerPacksOperation: PostboxCoding { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift index df9fbc310d..e69d9f2550 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramMediaFile.swift @@ -590,6 +590,40 @@ public final class TelegramMediaFile: Media, Equatable, Codable { return false } + public var isCustomEmoji: Bool { + var hasSticker = false + for attribute in self.attributes { + if case .CustomEmoji = attribute { + hasSticker = true + break + } + } + return hasSticker + } + + public var isPremiumEmoji: Bool { + for attribute in self.attributes { + if case let .CustomEmoji(isPremium, _, _) = attribute { + return isPremium + } + } + return false + } + + public var isVideoEmoji: Bool { + if self.mimeType == "video/webm" { + var hasSticker = false + for attribute in self.attributes { + if case .CustomEmoji = attribute { + hasSticker = true + break + } + } + return hasSticker + } + return false + } + public var hasLinkedStickers: Bool { for attribute in self.attributes { if case .HasLinkedStickers = attribute { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index a8f0eb91ea..043ec2659c 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -544,6 +544,8 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _): if (flags & (1 << 3)) != 0 { namespace = Namespaces.ItemCollection.CloudMaskPacks + } else if (flags & (1 << 7)) != 0 { + namespace = Namespaces.ItemCollection.CloudEmojiPacks } else { namespace = Namespaces.ItemCollection.CloudStickerPacks } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ArchivedStickerPacks.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ArchivedStickerPacks.swift index bbd76d3042..e9743b26ee 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ArchivedStickerPacks.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ArchivedStickerPacks.swift @@ -7,6 +7,7 @@ import SwiftSignalKit public enum ArchivedStickerPacksNamespace: Int32 { case stickers = 0 case masks = 1 + case emoji = 2 var itemCollectionNamespace: ItemCollectionId.Namespace { switch self { @@ -14,6 +15,8 @@ public enum ArchivedStickerPacksNamespace: Int32 { return Namespaces.ItemCollection.CloudStickerPacks case .masks: return Namespaces.ItemCollection.CloudMaskPacks + case .emoji: + return Namespaces.ItemCollection.CloudEmojiPacks } } } @@ -32,6 +35,8 @@ func _internal_archivedStickerPacks(account: Account, namespace: ArchivedSticker var flags: Int32 = 0 if case .masks = namespace { flags |= 1 << 0 + } else if case .emoji = namespace { + flags |= 1 << 1 } return account.network.request(Api.functions.messages.getArchivedStickers(flags: flags, offsetId: 0, limit: 200)) |> map { result -> [ArchivedStickerPackItem] in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift index 8c71c80536..3e475f6888 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/CachedStickerPack.swift @@ -154,7 +154,7 @@ func _internal_cachedStickerPack(postbox: Postbox, network: Network, reference: } func cachedStickerPack(transaction: Transaction, reference: StickerPackReference) -> (StickerPackCollectionInfo, [StickerPackItem], Bool)? { - let namespaces: [Int32] = [Namespaces.ItemCollection.CloudStickerPacks, Namespaces.ItemCollection.CloudMaskPacks] + let namespaces: [Int32] = [Namespaces.ItemCollection.CloudStickerPacks, Namespaces.ItemCollection.CloudMaskPacks, Namespaces.ItemCollection.CloudEmojiPacks] switch reference { case let .id(id, _): for namespace in namespaces { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift index 3331fa05b5..36f1f53365 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/ImportStickers.swift @@ -173,6 +173,8 @@ func _internal_createStickerSet(account: Account, title: String, shortName: Stri case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _): if (flags & (1 << 3)) != 0 { namespace = Namespaces.ItemCollection.CloudMaskPacks + } else if (flags & (1 << 7)) != 0 { + namespace = Namespaces.ItemCollection.CloudEmojiPacks } else { namespace = Namespaces.ItemCollection.CloudStickerPacks } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift index 0abcc50461..0aad8c5696 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/LoadedStickerPack.swift @@ -52,6 +52,8 @@ func updatedRemoteStickerPack(postbox: Postbox, network: Network, reference: Sti case let .stickerSet(flags, _, _, _, _, _, _, _, _, _, _): if (flags & (1 << 3)) != 0 { namespace = Namespaces.ItemCollection.CloudMaskPacks + } else if (flags & (1 << 7)) != 0 { + namespace = Namespaces.ItemCollection.CloudEmojiPacks } else { namespace = Namespaces.ItemCollection.CloudStickerPacks } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift index eecdae4f7b..556bd90a04 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPack.swift @@ -46,6 +46,9 @@ extension StickerPackCollectionInfo { if (flags & (1 << 6)) != 0 { setFlags.insert(.isVideo) } + if (flags & (1 << 7)) != 0 { + setFlags.insert(.isEmoji) + } var thumbnailRepresentation: TelegramMediaImageRepresentation? var immediateThumbnailData: Data? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPackInteractiveOperations.swift b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPackInteractiveOperations.swift index 4c40991bdd..46562bcf55 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPackInteractiveOperations.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Stickers/StickerPackInteractiveOperations.swift @@ -11,6 +11,8 @@ func _internal_addStickerPackInteractively(postbox: Postbox, info: StickerPackCo namespace = .stickers case Namespaces.ItemCollection.CloudMaskPacks: namespace = .masks + case Namespaces.ItemCollection.CloudEmojiPacks: + namespace = .emoji default: namespace = nil } @@ -57,6 +59,8 @@ func _internal_removeStickerPacksInteractively(postbox: Postbox, ids: [ItemColle namespace = .stickers case Namespaces.ItemCollection.CloudMaskPacks: namespace = .masks + case Namespaces.ItemCollection.CloudEmojiPacks: + namespace = .emoji default: namespace = nil } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index da1c28b02e..a8174639c5 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -276,6 +276,7 @@ public enum PresentationResourceKey: Int32 { case chatKeyboardActionButtonProfileIcon case chatKeyboardActionButtonAddToChatIcon case chatKeyboardActionButtonWebAppIcon + case chatEntityKeyboardLock case uploadToneIcon } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index e569f326f5..e7e93439fb 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1281,4 +1281,10 @@ public struct PresentationResourcesChat { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/BotWebApp"), color: theme.chat.inputButtonPanel.buttonTextColor) }) } + + public static func chatEntityKeyboardLock(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.chatEntityKeyboardLock.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Stickers/SmallLock"), color: theme.chat.inputMediaPanel.stickersSectionTextColor) + }) + } } diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 2a534c0b22..e22da66af9 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -69,7 +69,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { super.init() if let file = file { - self.updateFile(file: file, attemptSynchronousLoad: true) + self.updateFile(file: file, attemptSynchronousLoad: attemptSynchronousLoad) } else { self.infoDisposable = (context.engine.stickers.resolveInlineSticker(fileId: emoji.fileId) |> deliverOnMainQueue).start(next: { [weak self] file in @@ -130,10 +130,30 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { self.loadAnimation() } else { - self.loadDisposable = self.renderer.loadFirstFrame(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, completion: { [weak self] _ in - self?.loadAnimation() + let pointSize = self.pointSize + let placeholderColor = self.placeholderColor + self.loadDisposable = self.renderer.loadFirstFrame(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, completion: { [weak self] result in + if !result { + MultiAnimationRendererImpl.firstFrameQueue.async { + let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) + + DispatchQueue.main.async { + guard let strongSelf = self else { + return + } + if let image = image { + strongSelf.contents = image.cgImage + } + strongSelf.loadAnimation() + } + } + } else { + guard let strongSelf = self else { + return + } + strongSelf.loadAnimation() + } }) - self.loadAnimation() } } @@ -151,7 +171,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { return } - if file.isVideoSticker { + if file.isVideoEmoji { cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer) } else { guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { @@ -162,7 +182,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } }) - let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: file.resource).start() + let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: .customEmoji(media: file), resource: file.resource).start() return ActionDisposable { dataDisposable.dispose() diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index 353a3fa407..49ea2ba2bc 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -39,6 +39,8 @@ swift_library( "//submodules/StickerPackPreviewUI:StickerPackPreviewUI", "//submodules/UndoUI:UndoUI", "//submodules/Components/MultilineTextComponent:MultilineTextComponent", + "//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent", + "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 37500a96b5..09fd0b9db5 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -22,6 +22,8 @@ import ContextUI import PremiumUI import StickerPackPreviewUI import UndoUI +import AudioToolbox +import SolidRoundedButtonComponent private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white) @@ -52,6 +54,74 @@ private final class PremiumBadgeView: UIView { } } +private final class GroupHeaderLayer: SimpleLayer { + private var lockIconLayer: SimpleLayer? + + private var currentTextLayout: (string: String, color: UIColor, constrainedWidth: CGFloat, size: CGSize)? + + override init() { + super.init() + } + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(theme: PresentationTheme, title: String, isPremium: Bool, constrainedWidth: CGFloat) -> (size: CGSize, horizontalOffset: CGFloat) { + let color = theme.chat.inputMediaPanel.stickersSectionTextColor + + let horizontalOffset: CGFloat + if isPremium { + let lockIconLayer: SimpleLayer + if let current = self.lockIconLayer { + lockIconLayer = current + } else { + lockIconLayer = SimpleLayer() + self.lockIconLayer = lockIconLayer + self.addSublayer(lockIconLayer) + } + 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) + } else { + lockIconLayer.contents = nil + horizontalOffset = 0.0 + } + } else { + if let lockIconLayer = self.lockIconLayer { + self.lockIconLayer = nil + lockIconLayer.removeFromSuperlayer() + } + horizontalOffset = 0.0 + } + + if let currentTextLayout = self.currentTextLayout, currentTextLayout.string == title, currentTextLayout.color == color, currentTextLayout.constrainedWidth == constrainedWidth { + return (currentTextLayout.size, horizontalOffset) + } + + 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) + + return (size, horizontalOffset) + } +} + public final class EmojiPagerContentComponent: Component { public typealias EnvironmentType = (EntityKeyboardChildEnvironment, PagerComponentChildEnvironment) @@ -59,6 +129,7 @@ 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 pushController: (ViewController) -> Void public let presentController: (ViewController) -> Void public let presentGlobalOverlayController: (ViewController) -> Void @@ -70,6 +141,7 @@ public final class EmojiPagerContentComponent: Component { performItemAction: @escaping (Item, UIView, CGRect, CALayer) -> Void, deleteBackwards: @escaping () -> Void, openStickerSettings: @escaping () -> Void, + openPremiumSection: @escaping () -> Void, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController) -> Void, presentGlobalOverlayController: @escaping (ViewController) -> Void, @@ -80,6 +152,7 @@ public final class EmojiPagerContentComponent: Component { self.performItemAction = performItemAction self.deleteBackwards = deleteBackwards self.openStickerSettings = openStickerSettings + self.openPremiumSection = openPremiumSection self.pushController = pushController self.presentController = presentController self.presentGlobalOverlayController = presentGlobalOverlayController @@ -89,28 +162,43 @@ public final class EmojiPagerContentComponent: Component { } } + public enum StaticEmojiSegment: Int32, CaseIterable { + case people = 0 + case animalsAndNature = 1 + case foodAndDrink = 2 + case activityAndSport = 3 + case travelAndPlaces = 4 + case objects = 5 + case symbols = 6 + case flags = 7 + } + public final class Item: Equatable { - public let emoji: String - public let file: TelegramMediaFile - public let stickerPackItem: StickerPackItem? + public let file: TelegramMediaFile? + public let staticEmoji: String? + public let subgroupId: Int32? - public init(emoji: String, file: TelegramMediaFile, stickerPackItem: StickerPackItem?) { - self.emoji = emoji + public init( + file: TelegramMediaFile?, + staticEmoji: String?, + subgroupId: Int32? + ) { self.file = file - self.stickerPackItem = stickerPackItem + self.staticEmoji = staticEmoji + self.subgroupId = subgroupId } public static func ==(lhs: Item, rhs: Item) -> Bool { if lhs === rhs { return true } - if lhs.emoji != rhs.emoji { + if lhs.file?.fileId != rhs.file?.fileId { return false } - if lhs.file.fileId != rhs.file.fileId { + if lhs.staticEmoji != rhs.staticEmoji { return false } - if lhs.stickerPackItem?.file.fileId != rhs.stickerPackItem?.file.fileId { + if lhs.subgroupId != rhs.subgroupId { return false } @@ -119,27 +207,45 @@ public final class EmojiPagerContentComponent: Component { } public final class ItemGroup: Equatable { - public let id: AnyHashable + public let supergroupId: AnyHashable + public let groupId: AnyHashable public let title: String? + public let isPremium: Bool + public let displayPremiumBadges: Bool public let items: [Item] public init( - id: AnyHashable, + supergroupId: AnyHashable, + groupId: AnyHashable, title: String?, + isPremium: Bool, + displayPremiumBadges: Bool, items: [Item] ) { - self.id = id + self.supergroupId = supergroupId + self.groupId = groupId self.title = title + self.isPremium = isPremium + self.displayPremiumBadges = displayPremiumBadges self.items = items } public static func ==(lhs: ItemGroup, rhs: ItemGroup) -> Bool { - if lhs.id != rhs.id { + if lhs.supergroupId != rhs.supergroupId { + return false + } + if lhs.groupId != rhs.groupId { return false } if lhs.title != rhs.title { return false } + if lhs.isPremium != rhs.isPremium { + return false + } + if lhs.displayPremiumBadges != rhs.displayPremiumBadges { + return false + } if lhs.items != rhs.items { return false } @@ -214,14 +320,17 @@ public final class EmojiPagerContentComponent: Component { public final class View: UIView, UIScrollViewDelegate, ComponentTaggedView { private struct ItemGroupDescription: Equatable { - let id: AnyHashable + let supergroupId: AnyHashable + let groupId: AnyHashable let hasTitle: Bool + let isPremium: Bool let itemCount: Int } private struct ItemGroupLayout: Equatable { let frame: CGRect - let id: AnyHashable + let supergroupId: AnyHashable + let groupId: AnyHashable let itemTopOffset: CGFloat let itemCount: Int } @@ -238,10 +347,16 @@ public final class EmojiPagerContentComponent: Component { var itemsPerRow: Int var contentSize: CGSize - init(width: CGFloat, containerInsets: UIEdgeInsets, itemGroups: [ItemGroupDescription], itemLayoutType: ItemLayoutType) { + var premiumButtonInset: CGFloat + var premiumButtonHeight: CGFloat + + init(width: CGFloat, containerInsets: UIEdgeInsets, itemGroups: [ItemGroupDescription], itemLayoutType: ItemLayoutType, expandedPremiumGroups: Set) { self.width = width self.containerInsets = containerInsets + self.premiumButtonInset = 6.0 + self.premiumButtonHeight = 50.0 + let minItemsPerRow: Int let minSpacing: CGFloat switch itemLayoutType { @@ -276,10 +391,14 @@ public final class EmojiPagerContentComponent: Component { } let numRowsInGroup = (itemGroup.itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow - let groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.visibleItemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) + var groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.visibleItemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) + if itemGroup.isPremium && expandedPremiumGroups.contains(itemGroup.groupId) { + groupContentSize.height += self.premiumButtonInset + self.premiumButtonHeight + } self.itemGroupLayouts.append(ItemGroupLayout( frame: CGRect(origin: CGPoint(x: 0.0, y: verticalGroupOrigin), size: groupContentSize), - id: itemGroup.id, + supergroupId: itemGroup.supergroupId, + groupId: itemGroup.groupId, itemTopOffset: itemTopOffset, itemCount: itemGroup.itemCount )) @@ -307,8 +426,8 @@ public final class EmojiPagerContentComponent: Component { ) } - func visibleItems(for rect: CGRect) -> [(id: AnyHashable, groupIndex: Int, groupItems: Range)] { - var result: [(id: 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] @@ -326,7 +445,8 @@ public final class EmojiPagerContentComponent: Component { if maxVisibleIndex >= minVisibleIndex { result.append(( - id: group.id, + supergroupId: group.supergroupId, + groupId: group.groupId, groupIndex: groupIndex, groupItems: minVisibleIndex ..< (maxVisibleIndex + 1) )) @@ -394,12 +514,14 @@ public final class EmojiPagerContentComponent: Component { final class ItemLayer: MultiAnimationRenderTarget { struct Key: Hashable { var groupId: AnyHashable - var fileId: MediaId + var fileId: MediaId? + var staticEmoji: String? } let item: Item - private let file: TelegramMediaFile + private let file: TelegramMediaFile? + private let staticEmoji: String? private let placeholderColor: UIColor private let size: CGSize private var disposable: Disposable? @@ -422,7 +544,8 @@ public final class EmojiPagerContentComponent: Component { context: AccountContext, groupId: String, attemptSynchronousLoad: Bool, - file: TelegramMediaFile, + file: TelegramMediaFile?, + staticEmoji: String?, cache: AnimationCache, renderer: MultiAnimationRenderer, placeholderColor: UIColor, @@ -433,6 +556,7 @@ public final class EmojiPagerContentComponent: Component { ) { self.item = item self.file = file + self.staticEmoji = staticEmoji self.placeholderColor = placeholderColor self.onUpdateDisplayPlaceholder = onUpdateDisplayPlaceholder @@ -442,86 +566,107 @@ public final class EmojiPagerContentComponent: Component { super.init() - if file.isAnimatedSticker || file.isVideoSticker { - let loadAnimation: () -> Void = { [weak self] in - guard let strongSelf = self else { - return - } - - strongSelf.disposable = renderer.add(groupId: groupId, target: strongSelf, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in - let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) + if let file = file { + if file.isAnimatedSticker || file.isVideoEmoji { + let loadAnimation: () -> Void = { [weak self] in + guard let strongSelf = self else { + return + } - let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in - guard let result = result else { - return - } + strongSelf.disposable = renderer.add(groupId: groupId, target: strongSelf, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, fetch: { size, writer in + let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) - if file.isVideoSticker { - cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer) - } else { - guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { - writer.finish() + let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in + guard let result = result else { return } - cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) + + if file.isVideoEmoji { + cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer) + } else if file.isAnimatedSticker { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { + writer.finish() + return + } + cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) + } else { + cacheStillSticker(path: result, width: Int(size.width), height: Int(size.height), writer: writer) + } + }) + + let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: file.resource).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() } }) - - let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: file.resource).start() - - return ActionDisposable { - dataDisposable.dispose() - fetchDisposable.dispose() - } - }) - } - - if attemptSynchronousLoad { - if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { - self.updateDisplayPlaceholder(displayPlaceholder: true) } - loadAnimation() - } else { - let _ = renderer.loadFirstFrame(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { [weak self] success in + if attemptSynchronousLoad { + if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { + self.updateDisplayPlaceholder(displayPlaceholder: true) + } + loadAnimation() - - if !success { - guard let strongSelf = self else { - return - } + } else { + let _ = renderer.loadFirstFrame(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize, completion: { [weak self] success in + loadAnimation() - strongSelf.updateDisplayPlaceholder(displayPlaceholder: true) + if !success { + guard let strongSelf = self else { + return + } + + strongSelf.updateDisplayPlaceholder(displayPlaceholder: true) + } + }) + } + } else if let dimensions = file.dimensions { + let isSmall: Bool = false + self.disposable = (chatMessageSticker(account: context.account, file: file, small: isSmall, synchronousLoad: attemptSynchronousLoad)).start(next: { [weak self] resultTransform in + let boundingSize = CGSize(width: 93.0, height: 93.0) + let imageSize = dimensions.cgSize.aspectFilled(boundingSize) + + if let image = resultTransform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() { + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + + strongSelf.contents = image.cgImage + } } }) - } - } else if let dimensions = file.dimensions { - let isSmall: Bool = false - self.disposable = (chatMessageSticker(account: context.account, file: file, small: isSmall, synchronousLoad: attemptSynchronousLoad)).start(next: { [weak self] resultTransform in - let boundingSize = CGSize(width: 93.0, height: 93.0) - let imageSize = dimensions.cgSize.aspectFilled(boundingSize) - if let image = resultTransform(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), resizeMode: .fill(.clear)))?.generateImage() { - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - - strongSelf.contents = image.cgImage - } - } - }) + self.fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: stickerPackFileReference(file), resource: chatMessageStickerResource(file: file, small: isSmall)).start() + } - 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) + 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 scaledSize = CGSize(width: floor(size.width * preScaleFactor), height: floor(size.height * preScaleFactor)) + let scaleFactor = scaledSize.width / size.width + + context.scaleBy(x: 1.0 / scaleFactor, y: 1.0 / scaleFactor) + + let string = NSAttributedString(string: staticEmoji, font: Font.regular(floor(30.0 * scaleFactor)), textColor: .black) + let boundingRect = string.boundingRect(with: scaledSize, options: .usesLineFragmentOrigin, context: nil) + UIGraphicsPushContext(context) + string.draw(at: CGPoint(x: (scaledSize.width - boundingRect.width) / 2.0 + boundingRect.minX, y: (scaledSize.height - boundingRect.height) / 2.0 + boundingRect.minY)) + UIGraphicsPopContext() + }) + self.contents = image?.cgImage } } @@ -533,6 +678,7 @@ public final class EmojiPagerContentComponent: Component { self.item = layer.item self.file = layer.file + self.staticEmoji = layer.staticEmoji self.placeholderColor = layer.placeholderColor self.size = layer.size @@ -616,6 +762,24 @@ public final class EmojiPagerContentComponent: Component { } } + private final class GroupBorderLayer: CAShapeLayer { + override func action(forKey event: String) -> CAAction? { + return nullAction + } + + override init() { + super.init() + } + + override init(layer: Any) { + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + private final class ContentScrollView: UIScrollView, PagerExpandableScrollView { } @@ -628,18 +792,22 @@ public final class EmojiPagerContentComponent: Component { private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:] private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:] - private var visibleGroupHeaders: [AnyHashable: ComponentView] = [:] + private var visibleGroupHeaders: [AnyHashable: GroupHeaderLayer] = [:] + private var visibleGroupBorders: [AnyHashable: GroupBorderLayer] = [:] + private var visibleGroupPremiumButtons: [AnyHashable: ComponentView] = [:] + private var expandedPremiumGroups: Set = Set() private var ignoreScrolling: Bool = false private var keepTopPanelVisibleUntilScrollingInput: Bool = false private var component: EmojiPagerContentComponent? + private weak var state: EmptyComponentState? private var pagerEnvironment: PagerComponentChildEnvironment? private var theme: PresentationTheme? private var activeItemUpdated: ActionSlot<(AnyHashable, Transition)>? private var itemLayout: ItemLayout? + private var peekRecognizer: PeekControllerGestureRecognizer? private var currentContextGestureItemKey: ItemLayer.Key? - private weak var peekController: PeekController? override init(frame: CGRect) { @@ -674,42 +842,20 @@ public final class EmojiPagerContentComponent: Component { self.scrollView.clipsToBounds = false self.addSubview(self.scrollView) - //self.clipsToBounds = true - self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - /*self.useSublayerTransformForActivation = false - self.shouldBegin = { [weak self] point in - guard let strongSelf = self else { - return false - } - if let item = strongSelf.item(atPoint: point), let itemLayer = strongSelf.visibleItemLayers[item.1] { - strongSelf.currentContextGestureItemKey = item.1 - strongSelf.targetLayerForActivationProgress = itemLayer - return true - } else { - return false - } - } - self.contextGesture?.cancelGesturesOnActivation = { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.scrollView.panGestureRecognizer.state = .failed - }*/ - let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in guard let strongSelf = self, let component = strongSelf.component else { return nil } - guard let item = strongSelf.item(atPoint: point), let itemLayer = strongSelf.visibleItemLayers[item.1] else { + guard let item = strongSelf.item(atPoint: point), let itemLayer = strongSelf.visibleItemLayers[item.1], let file = item.0.file else { return nil } let context = component.context let accountPeerId = context.account.peerId return combineLatest( - context.engine.stickers.isStickerSaved(id: item.0.file.fileId), + context.engine.stickers.isStickerSaved(id: file.fileId), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId)) |> map { peer -> Bool in var hasPremium = false if case let .user(user) = peer, user.isPremium { @@ -734,9 +880,9 @@ public final class EmojiPagerContentComponent: Component { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - sendSticker(.standalone(media: item.0.file), true, false, nil, false, animationNode.view, animationNode.bounds, nil) + sendSticker(.standalone(media: file), true, false, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - sendSticker(.standalone(media: item.0.file), true, false, nil, false, imageNode.view, imageNode.bounds, nil) + sendSticker(.standalone(media: file), true, false, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -748,9 +894,9 @@ public final class EmojiPagerContentComponent: Component { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = sendSticker(.standalone(media: item.0.file), false, true, nil, false, animationNode.view, animationNode.bounds, nil) + let _ = sendSticker(.standalone(media: file), false, true, nil, false, animationNode.view, animationNode.bounds, nil) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = sendSticker(.standalone(media: item.0.file), false, true, nil, false, imageNode.view, imageNode.bounds, nil) + let _ = sendSticker(.standalone(media: file), false, true, nil, false, imageNode.view, imageNode.bounds, nil) } } f(.default) @@ -762,11 +908,11 @@ public final class EmojiPagerContentComponent: Component { f(.default) let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let _ = (context.engine.stickers.toggleStickerSaved(file: item.0.file, saved: !isStarred) + let _ = (context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred) |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - component.inputInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: item.0.file, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: false, action: { _ in return false })) + component.inputInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false })) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let text: String @@ -775,7 +921,7 @@ public final class EmojiPagerContentComponent: Component { } else { text = presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } - component.inputInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: item.0.file, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: false, action: { action in + component.inputInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { action in if case .info = action { let controller = PremiumIntroScreen(context: context, source: .savedStickers) component.inputInteraction.pushController(controller) @@ -793,9 +939,9 @@ public final class EmojiPagerContentComponent: Component { }, action: { _, f in f(.default) - loop: for attribute in item.0.file.attributes { + loop: for attribute in file.attributes { switch attribute { - case let .Sticker(_, packReference, _): + case let .CustomEmoji(_, _, packReference): if let packReference = packReference { let controller = StickerPackScreen(context: context, updatedPresentationData: nil, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: component.inputInteraction.navigationController(), sendSticker: { file, sourceView, sourceRect in //return component.inputInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect, nil) @@ -813,7 +959,7 @@ public final class EmojiPagerContentComponent: Component { })) ) - return (strongSelf, strongSelf.scrollView.convert(itemLayer.frame, to: strongSelf), StickerPreviewPeekContent(account: context.account, theme: presentationData.theme, strings: presentationData.strings, item: .pack(item.0.file), isLocked: item.0.file.isPremiumSticker && !hasPremium, menu: menuItems, openPremiumIntro: { + return (strongSelf, strongSelf.scrollView.convert(itemLayer.frame, to: strongSelf), StickerPreviewPeekContent(account: context.account, theme: presentationData.theme, strings: presentationData.strings, item: .pack(file), isLocked: file.isPremiumSticker && !hasPremium, menu: menuItems, openPremiumIntro: { let controller = PremiumIntroScreen(context: context, source: .stickers) component.inputInteraction.pushController(controller) })) @@ -848,7 +994,9 @@ public final class EmojiPagerContentComponent: Component { } strongSelf.updatePreviewingItem(item: item, animated: true)*/ }) + self.peekRecognizer = peekRecognizer self.addGestureRecognizer(peekRecognizer) + self.peekRecognizer?.isEnabled = false } required init?(coder: NSCoder) { @@ -864,12 +1012,28 @@ public final class EmojiPagerContentComponent: Component { return false } - public func scrollToItemGroup(groupId: AnyHashable) { - guard let itemLayout = self.itemLayout else { + public func scrollToItemGroup(id supergroupId: AnyHashable, subgroupId: Int32?) { + guard let component = self.component, let itemLayout = self.itemLayout else { return } - for group in itemLayout.itemGroupLayouts { - if group.id == groupId { + for groupIndex in 0 ..< itemLayout.itemGroupLayouts.count { + let group = itemLayout.itemGroupLayouts[groupIndex] + + var subgroupItemIndex: Int? + if group.supergroupId == supergroupId { + if let subgroupId = subgroupId { + inner: for itemGroup in component.itemGroups { + if itemGroup.supergroupId == supergroupId { + for i in 0 ..< itemGroup.items.count { + if itemGroup.items[i].subgroupId == subgroupId { + subgroupItemIndex = i + break + } + } + break inner + } + } + } let wasIgnoringScrollingEvents = self.ignoreScrolling self.ignoreScrolling = true self.scrollView.setContentOffset(self.scrollView.contentOffset, animated: false) @@ -877,14 +1041,56 @@ public final class EmojiPagerContentComponent: Component { self.keepTopPanelVisibleUntilScrollingInput = true - self.scrollView.scrollRectToVisible(CGRect(origin: group.frame.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) + let anchorFrame: CGRect + if let subgroupItemIndex = subgroupItemIndex { + anchorFrame = itemLayout.frame(groupIndex: groupIndex, itemIndex: subgroupItemIndex) + } else { + 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) } } } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + guard let component = self.component else { + return + } if case .ended = recognizer.state { - if let component = self.component, let (item, itemKey) = self.item(atPoint: recognizer.location(in: self)), let itemLayer = self.visibleItemLayers[itemKey] { + 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) { + for group in component.itemGroups { + if group.groupId == id { + if group.isPremium && !self.expandedPremiumGroups.contains(id) { + if self.expandedPremiumGroups.contains(id) { + self.expandedPremiumGroups.remove(id) + } else { + self.expandedPremiumGroups.insert(id) + } + + let previousItemLayout = self.itemLayout + + let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + self.state?.updated(transition: transition) + + if let previousItemLayout = previousItemLayout, let itemLayout = self.itemLayout { + let boundsOffset = itemLayout.contentSize.height - previousItemLayout.contentSize.height + self.scrollView.setContentOffset(CGPoint(x: 0.0, y: self.scrollView.contentOffset.y + boundsOffset), animated: false) + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -boundsOffset), to: CGPoint(), additive: true) + } + + return + } else { + break outer + } + } + } + } + } + + if let (item, itemKey) = self.item(atPoint: recognizer.location(in: self)), let itemLayer = self.visibleItemLayers[itemKey] { component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemLayer.frame, to: self), itemLayer) } } @@ -926,7 +1132,7 @@ public final class EmojiPagerContentComponent: Component { return } - self.updateVisibleItems(attemptSynchronousLoads: false) + self.updateVisibleItems(transition: .immediate, attemptSynchronousLoads: false) self.updateScrollingOffset(isReset: false, transition: .immediate) } @@ -1000,7 +1206,7 @@ public final class EmojiPagerContentComponent: Component { self.updateScrollingOffset(isReset: false, transition: transition) } - private func updateVisibleItems(attemptSynchronousLoads: Bool) { + private func updateVisibleItems(transition: Transition, attemptSynchronousLoads: Bool) { guard let component = self.component, let theme = self.theme, let itemLayout = self.itemLayout else { return } @@ -1009,6 +1215,8 @@ public final class EmojiPagerContentComponent: Component { var validIds = Set() var validGroupHeaderIds = Set() + var validGroupBorderIds = Set() + var validGroupPremiumButtonIds = Set() let effectiveVisibleBounds = CGRect(origin: self.scrollView.bounds.origin, size: self.effectiveVisibleSize) let topVisibleDetectionBounds = effectiveVisibleBounds.offsetBy(dx: 0.0, dy: 41.0) @@ -1018,49 +1226,180 @@ public final class EmojiPagerContentComponent: Component { let itemGroupLayout = itemLayout.itemGroupLayouts[groupItems.groupIndex] if topVisibleGroupId == nil && itemGroupLayout.frame.intersects(topVisibleDetectionBounds) { - topVisibleGroupId = groupItems.id + topVisibleGroupId = groupItems.supergroupId } + var headerSize: CGSize? + var headerSizeUpdated = false if let title = itemGroup.title { - validGroupHeaderIds.insert(itemGroup.id) - let groupHeaderView: ComponentView - if let current = self.visibleGroupHeaders[itemGroup.id] { - groupHeaderView = current + validGroupHeaderIds.insert(itemGroup.groupId) + let groupHeaderLayer: GroupHeaderLayer + var groupHeaderTransition = transition + if let current = self.visibleGroupHeaders[itemGroup.groupId] { + groupHeaderLayer = current } else { - groupHeaderView = ComponentView() - self.visibleGroupHeaders[itemGroup.id] = groupHeaderView + groupHeaderTransition = .immediate + groupHeaderLayer = GroupHeaderLayer() + self.visibleGroupHeaders[itemGroup.groupId] = groupHeaderLayer + self.scrollView.layer.addSublayer(groupHeaderLayer) } - let groupHeaderSize = groupHeaderView.update( - transition: .immediate, - component: AnyComponent(Text( - text: title.uppercased(), font: Font.medium(12.0), color: theme.chat.inputMediaPanel.stickersSectionTextColor - )), - environment: {}, - containerSize: CGSize(width: itemLayout.contentSize.width - itemLayout.containerInsets.left - itemLayout.containerInsets.right, height: 100.0) - ) - if let view = groupHeaderView.view { - if view.superview == nil { - self.scrollView.addSubview(view) + let (groupHeaderSize, groupHeaderHorizontalOffset) = groupHeaderLayer.update(theme: theme, title: title, isPremium: itemGroup.isPremium, constrainedWidth: itemLayout.contentSize.width - itemLayout.containerInsets.left - itemLayout.containerInsets.right - 32.0) + + 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) + 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) + } + + if itemGroup.isPremium { + validGroupBorderIds.insert(itemGroup.groupId) + let groupBorderLayer: GroupBorderLayer + var groupBorderTransition = transition + if let current = self.visibleGroupBorders[itemGroup.groupId] { + groupBorderLayer = current + } else { + groupBorderTransition = .immediate + groupBorderLayer = GroupBorderLayer() + self.visibleGroupBorders[itemGroup.groupId] = groupBorderLayer + self.scrollView.layer.insertSublayer(groupBorderLayer, at: 0) + + groupBorderLayer.strokeColor = theme.chat.inputMediaPanel.stickersSectionTextColor.cgColor + groupBorderLayer.lineWidth = 1.6 + groupBorderLayer.lineCap = .round + groupBorderLayer.fillColor = nil + } + + let groupBorderHorizontalInset: CGFloat = itemLayout.containerInsets.left - 4.0 + let groupBorderVerticalTopOffset: CGFloat = 8.0 + let groupBorderVerticalInset: CGFloat = 6.0 + + 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 { + headerWidth = headerSize.width + 14.0 + } else { + headerWidth = 0.0 + } + let path = CGMutablePath() + 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) + path.addLine(to: CGPoint(x: 0.0, y: groupBorderFrame.height - radius)) + path.addArc(tangent1End: CGPoint(x: 0.0, y: groupBorderFrame.height), tangent2End: CGPoint(x: radius, y: groupBorderFrame.height), radius: radius) + path.addLine(to: CGPoint(x: groupBorderFrame.width - radius, y: groupBorderFrame.height)) + path.addArc(tangent1End: CGPoint(x: groupBorderFrame.width, y: groupBorderFrame.height), tangent2End: CGPoint(x: groupBorderFrame.width, y: groupBorderFrame.height - radius), radius: radius) + path.addLine(to: CGPoint(x: groupBorderFrame.width, y: radius)) + path.addArc(tangent1End: CGPoint(x: groupBorderFrame.width, y: 0.0), tangent2End: CGPoint(x: groupBorderFrame.width - radius, y: 0.0), radius: radius) + path.addLine(to: CGPoint(x: floor((groupBorderFrame.width - headerWidth) / 2.0) + headerWidth, y: 0.0)) + + let pathLength = (2.0 * groupBorderFrame.width + 2.0 * groupBorderFrame.height - 8.0 * radius + 2.0 * .pi * radius) - headerWidth + + var numberOfDashes = Int(floor(pathLength / 6.0)) + if numberOfDashes % 2 == 0 { + numberOfDashes -= 1 + } + let wholeLength = 6.0 * CGFloat(numberOfDashes) + let remainingLength = pathLength - wholeLength + let dashSpace = remainingLength / CGFloat(numberOfDashes) + + groupBorderTransition.setShapeLayerPath(layer: groupBorderLayer, path: path) + groupBorderTransition.setShapeLayerLineDashPattern(layer: groupBorderLayer, pattern: [(5.0 + dashSpace) as NSNumber, (7.0 + dashSpace) as NSNumber]) + } + groupBorderTransition.setFrame(layer: groupBorderLayer, frame: groupBorderFrame) + + if self.expandedPremiumGroups.contains(itemGroup.groupId) { + validGroupPremiumButtonIds.insert(itemGroup.groupId) + + let groupPremiumButton: ComponentView + var groupPremiumButtonTransition = transition + if let current = self.visibleGroupPremiumButtons[itemGroup.groupId] { + groupPremiumButton = current + } else { + groupPremiumButtonTransition = .immediate + groupPremiumButton = ComponentView() + self.visibleGroupPremiumButtons[itemGroup.groupId] = groupPremiumButton + } + + //TODO:localize + let groupPremiumButtonSize = groupPremiumButton.update( + transition: groupPremiumButtonTransition, + component: AnyComponent(SolidRoundedButtonComponent( + title: "Unlock \(itemGroup.title ?? "Emoji")", + theme: SolidRoundedButtonComponent.Theme( + backgroundColor: .black, + backgroundColors: [ + UIColor(rgb: 0x0077ff), + UIColor(rgb: 0x6b93ff), + UIColor(rgb: 0x8878ff), + UIColor(rgb: 0xe46ace) + ], + foregroundColor: .white + ), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: radius, + gloss: true, + animationName: "premium_unlock", + iconPosition: .right, + iconSpacing: 4.0, + action: { [weak self] in + guard let strongSelf = self, let component = strongSelf.component else { + return + } + component.inputInteraction.openPremiumSection() + } + )), + environment: {}, + containerSize: CGSize(width: itemLayout.width - itemLayout.containerInsets.left - itemLayout.containerInsets.right, height: itemLayout.premiumButtonHeight) + ) + let groupPremiumButtonFrame = CGRect(origin: CGPoint(x: itemLayout.containerInsets.left, y: itemGroupLayout.frame.maxY - groupPremiumButtonSize.height + 1.0), size: groupPremiumButtonSize) + if let view = groupPremiumButton.view { + var animateIn = false + if view.superview == nil { + view.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0) + animateIn = true + self.scrollView.addSubview(view) + } + groupPremiumButtonTransition.setFrame(view: view, frame: groupPremiumButtonFrame) + if animateIn, !transition.animation.isImmediate { + view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + transition.animateScale(view: view, from: 0.01, to: 1.0) + } } - view.frame = CGRect(origin: CGPoint(x: itemLayout.containerInsets.left, y: itemGroupLayout.frame.minY + 1.0), size: groupHeaderSize) } } for index in groupItems.groupItems.lowerBound ..< groupItems.groupItems.upperBound { let item = itemGroup.items[index] - let itemId = ItemLayer.Key(groupId: itemGroup.id, fileId: item.file.fileId) + let itemId = ItemLayer.Key(groupId: itemGroup.groupId, fileId: item.file?.fileId, staticEmoji: item.staticEmoji) validIds.insert(itemId) - let itemDimensions = item.file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) + 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, @@ -1068,17 +1407,18 @@ public final class EmojiPagerContentComponent: Component { groupId: "keyboard-\(Int(itemLayout.nativeItemSize))", 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: true, + displayPremiumBadgeIfAvailable: itemGroup.displayPremiumBadges, pointSize: itemNativeFitSize, onUpdateDisplayPlaceholder: { [weak self] displayPlaceholder in guard let strongSelf = self else { return } - if displayPlaceholder { + if displayPlaceholder, let file = item.file { if let itemLayer = strongSelf.visibleItemLayers[itemId] { let placeholderView: ItemPlaceholderView if let current = strongSelf.visibleItemPlaceholderViews[itemId] { @@ -1086,7 +1426,7 @@ public final class EmojiPagerContentComponent: Component { } else { placeholderView = ItemPlaceholderView( context: component.context, - file: item.file, + file: file, shimmerView: strongSelf.shimmerHostView, color: nil, size: itemNativeFitSize @@ -1121,16 +1461,12 @@ public final class EmojiPagerContentComponent: Component { let itemPosition = CGPoint(x: itemFrame.midX, y: itemFrame.midY) let itemBounds = CGRect(origin: CGPoint(), size: itemFrame.size) - if itemLayer.position != itemPosition { - itemLayer.position = itemPosition - } - if itemLayer.bounds != itemBounds { - itemLayer.bounds = 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 { - placeholderView.frame = itemFrame + itemTransition.setFrame(view: placeholderView, frame: itemFrame) placeholderView.update(size: itemFrame.size) } } else if updateItemLayerPlaceholder { @@ -1159,16 +1495,38 @@ public final class EmojiPagerContentComponent: Component { } var removedGroupHeaderIds: [AnyHashable] = [] - for (id, groupHeaderView) in self.visibleGroupHeaders { + for (id, groupHeaderLayer) in self.visibleGroupHeaders { if !validGroupHeaderIds.contains(id) { removedGroupHeaderIds.append(id) - groupHeaderView.view?.removeFromSuperview() + groupHeaderLayer.removeFromSuperlayer() } } for id in removedGroupHeaderIds { self.visibleGroupHeaders.removeValue(forKey: id) } + var removedGroupBorderIds: [AnyHashable] = [] + for (id, groupBorderLayer) in self.visibleGroupBorders { + if !validGroupBorderIds.contains(id) { + removedGroupBorderIds.append(id) + groupBorderLayer.removeFromSuperlayer() + } + } + for id in removedGroupBorderIds { + self.visibleGroupBorders.removeValue(forKey: id) + } + + var removedGroupPremiumButtonIds: [AnyHashable] = [] + for (id, groupPremiumButton) in self.visibleGroupPremiumButtons { + if !validGroupPremiumButtonIds.contains(id) { + removedGroupPremiumButtonIds.append(id) + groupPremiumButton.view?.removeFromSuperview() + } + } + for id in removedGroupPremiumButtonIds { + self.visibleGroupPremiumButtons.removeValue(forKey: id) + } + if let topVisibleGroupId = topVisibleGroupId { self.activeItemUpdated?.invoke((topVisibleGroupId, .immediate)) } @@ -1184,6 +1542,9 @@ public final class EmojiPagerContentComponent: Component { func update(component: EmojiPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component + self.state = state + + self.peekRecognizer?.isEnabled = component.itemLayoutType == .detailed let keyboardChildEnvironment = environment[EntityKeyboardChildEnvironment.self].value @@ -1202,13 +1563,24 @@ public final class EmojiPagerContentComponent: Component { var itemGroups: [ItemGroupDescription] = [] for itemGroup in component.itemGroups { itemGroups.append(ItemGroupDescription( - id: itemGroup.id, + supergroupId: itemGroup.supergroupId, + groupId: itemGroup.groupId, hasTitle: itemGroup.title != nil, + isPremium: itemGroup.isPremium, itemCount: itemGroup.items.count )) } - let itemLayout = ItemLayout(width: availableSize.width, containerInsets: UIEdgeInsets(top: pagerEnvironment.containerInsets.top + 9.0, left: pagerEnvironment.containerInsets.left + 12.0, bottom: 9.0 + pagerEnvironment.containerInsets.bottom, right: pagerEnvironment.containerInsets.right + 12.0), itemGroups: itemGroups, itemLayoutType: component.itemLayoutType) + var itemTransition = transition + + let itemLayout = ItemLayout(width: availableSize.width, containerInsets: UIEdgeInsets(top: pagerEnvironment.containerInsets.top + 9.0, left: pagerEnvironment.containerInsets.left + 12.0, bottom: 9.0 + pagerEnvironment.containerInsets.bottom, right: pagerEnvironment.containerInsets.right + 12.0), itemGroups: itemGroups, itemLayoutType: component.itemLayoutType, expandedPremiumGroups: expandedPremiumGroups) + if let previousItemLayout = self.itemLayout { + if previousItemLayout.width != itemLayout.width { + itemTransition = .immediate + } + } else { + itemTransition = .immediate + } self.itemLayout = itemLayout self.ignoreScrolling = true @@ -1229,7 +1601,7 @@ public final class EmojiPagerContentComponent: Component { let effectiveVisibleSize = strongSelf.scrollView.bounds.size if strongSelf.effectiveVisibleSize != effectiveVisibleSize { strongSelf.effectiveVisibleSize = effectiveVisibleSize - strongSelf.updateVisibleItems(attemptSynchronousLoads: false) + strongSelf.updateVisibleItems(transition: .immediate, attemptSynchronousLoads: false) } }) } @@ -1243,7 +1615,7 @@ public final class EmojiPagerContentComponent: Component { self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: scrollView.isDragging || scrollView.isDecelerating) self.ignoreScrolling = false - self.updateVisibleItems(attemptSynchronousLoads: !(scrollView.isDragging || scrollView.isDecelerating)) + self.updateVisibleItems(transition: itemTransition, attemptSynchronousLoads: !(scrollView.isDragging || scrollView.isDecelerating)) return availableSize } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 27c9e26502..33c1f5cbfc 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -8,6 +8,8 @@ import TelegramCore import Postbox import BlurredBackgroundComponent import BundleIconComponent +import AudioToolbox +import SwiftSignalKit public final class EntityKeyboardChildEnvironment: Equatable { public let theme: PresentationTheme @@ -41,6 +43,11 @@ public final class EntityKeyboardComponent: Component { } } + private enum ReorderCategory { + case stickers + case emoji + } + public struct GifSearchEmoji: Equatable { public var emoji: String public var file: TelegramMediaFile @@ -199,6 +206,7 @@ public final class EntityKeyboardComponent: Component { //TODO:localize topGifItems.append(EntityKeyboardTopPanelComponent.Item( id: "recent", + isReorderable: false, content: AnyComponent(EntityKeyboardIconTopPanelComponent( imageName: "Chat/Input/Media/RecentTabIcon", theme: component.theme, @@ -210,6 +218,7 @@ public final class EntityKeyboardComponent: Component { )) topGifItems.append(EntityKeyboardTopPanelComponent.Item( id: "trending", + isReorderable: false, content: AnyComponent(EntityKeyboardIconTopPanelComponent( imageName: "Chat/Input/Media/TrendingGifs", theme: component.theme, @@ -222,6 +231,7 @@ public final class EntityKeyboardComponent: Component { for emoji in component.availableGifSearchEmojies { topGifItems.append(EntityKeyboardTopPanelComponent.Item( id: emoji.emoji, + isReorderable: false, content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( context: component.stickerContent.context, file: emoji.file, @@ -248,7 +258,9 @@ public final class EntityKeyboardComponent: Component { theme: component.theme, items: topGifItems, defaultActiveItemId: defaultActiveGifItemId, - activeContentItemIdUpdated: gifsContentItemIdUpdated + activeContentItemIdUpdated: gifsContentItemIdUpdated, + reorderItems: { _ in + } )))) contentIcons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputGifsIcon", @@ -265,56 +277,53 @@ public final class EntityKeyboardComponent: Component { self?.openSearch() } ).minSize(CGSize(width: 38.0, height: 38.0))))) - /*contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "gifs", component: AnyComponent(Button( - content: AnyComponent(BundleIconComponent( - name: "Chat/Input/Media/EntityInputSettingsIcon", - tintColor: component.theme.chat.inputMediaPanel.panelIconColor, - maxSize: nil - )), - action: { - } - ).minSize(CGSize(width: 38.0, height: 38.0)))))*/ var topStickerItems: [EntityKeyboardTopPanelComponent.Item] = [] for itemGroup in component.stickerContent.itemGroups { - if let id = itemGroup.id.base as? String { + if let id = itemGroup.supergroupId.base as? String { let iconMapping: [String: String] = [ + "saved": "Chat/Input/Media/SavedStickersTabIcon", "recent": "Chat/Input/Media/RecentTabIcon", "premium": "Chat/Input/Media/PremiumIcon" ] let titleMapping: [String: String] = [ + "saved": "Saved", "recent": "Recent", "premium": "Premium" ] if let iconName = iconMapping[id], let title = titleMapping[id] { topStickerItems.append(EntityKeyboardTopPanelComponent.Item( - id: itemGroup.id, + id: itemGroup.supergroupId, + isReorderable: false, content: AnyComponent(EntityKeyboardIconTopPanelComponent( imageName: iconName, theme: component.theme, title: title, pressed: { [weak self] in - self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.id) + self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) } )) )) } } else { if !itemGroup.items.isEmpty { - topStickerItems.append(EntityKeyboardTopPanelComponent.Item( - id: itemGroup.id, - content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( - context: component.stickerContent.context, - file: itemGroup.items[0].file, - animationCache: component.stickerContent.animationCache, - animationRenderer: component.stickerContent.animationRenderer, - theme: component.theme, - title: itemGroup.title ?? "", - pressed: { [weak self] in - self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.id) - } + if let file = itemGroup.items[0].file { + topStickerItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: true, + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: component.stickerContent.context, + file: file, + animationCache: component.stickerContent.animationCache, + animationRenderer: component.stickerContent.animationRenderer, + theme: component.theme, + title: itemGroup.title ?? "", + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) + } + )) )) - )) + } } } } @@ -323,7 +332,13 @@ public final class EntityKeyboardComponent: Component { contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, items: topStickerItems, - activeContentItemIdUpdated: stickersContentItemIdUpdated + activeContentItemIdUpdated: stickersContentItemIdUpdated, + reorderItems: { [weak self] items in + guard let strongSelf = self else { + return + } + strongSelf.reorderPacks(category: .stickers, items: items) + } )))) contentIcons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputStickersIcon", @@ -356,26 +371,53 @@ public final class EntityKeyboardComponent: Component { var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] for itemGroup in component.emojiContent.itemGroups { if !itemGroup.items.isEmpty { - topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( - id: itemGroup.id, - content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( - context: component.emojiContent.context, - file: itemGroup.items[0].file, - animationCache: component.emojiContent.animationCache, - animationRenderer: component.emojiContent.animationRenderer, - theme: component.theme, - title: itemGroup.title ?? "", - pressed: { [weak self] in - self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.id) - } - )) - )) + if let id = itemGroup.groupId.base as? String { + if id == "static" { + topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: false, + content: AnyComponent(EntityKeyboardStaticStickersPanelComponent( + theme: component.theme, + pressed: { [weak self] subgroupId in + guard let strongSelf = self else { + return + } + strongSelf.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: subgroupId.rawValue) + } + )) + )) + } + } else { + if let file = itemGroup.items[0].file { + topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: true, + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: component.emojiContent.context, + file: file, + animationCache: component.emojiContent.animationCache, + animationRenderer: component.emojiContent.animationRenderer, + theme: component.theme, + title: itemGroup.title ?? "", + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "emoji", groupId: itemGroup.supergroupId, subgroupId: nil) + } + )) + )) + } + } } } contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( theme: component.theme, items: topEmojiItems, - activeContentItemIdUpdated: emojiContentItemIdUpdated + activeContentItemIdUpdated: emojiContentItemIdUpdated, + reorderItems: { [weak self] items in + guard let strongSelf = self else { + return + } + strongSelf.reorderPacks(category: .emoji, items: items) + } )))) contentIcons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(BundleIconComponent( name: "Chat/Input/Media/EntityInputEmojiIcon", @@ -404,9 +446,11 @@ public final class EntityKeyboardComponent: Component { )), action: { deleteBackwards() + AudioServicesPlaySystemSound(1155) } ).withHoldAction({ deleteBackwards() + AudioServicesPlaySystemSound(1155) }).minSize(CGSize(width: 38.0, height: 38.0))))) let panelHideBehavior: PagerComponentPanelHideBehavior @@ -441,6 +485,7 @@ public final class EntityKeyboardComponent: Component { bottomInset: component.bottomInset, deleteBackwards: { [weak self] in self?.component?.emojiContent.inputInteraction.deleteBackwards() + AudioServicesPlaySystemSound(0x451) } )), panelStateUpdated: { [weak self] panelState, transition in @@ -599,11 +644,39 @@ public final class EntityKeyboardComponent: Component { component.hideInputUpdated(false, false, Transition(animation: .curve(duration: 0.4, curve: .spring))) } - private func scrollToItemGroup(contentId: String, groupId: AnyHashable) { + 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(groupId: groupId) + pagerView.scrollToItemGroup(id: groupId, subgroupId: subgroupId) } } + + private func reorderPacks(category: ReorderCategory, items: [EntityKeyboardTopPanelComponent.Item]) { + guard let component = self.component else { + return + } + + var currentIds: [ItemCollectionId] = [] + for item in items { + guard let id = item.id.base as? ItemCollectionId else { + continue + } + currentIds.append(id) + } + let namespace: ItemCollectionId.Namespace + switch category { + case .stickers: + namespace = Namespaces.ItemCollection.CloudStickerPacks + case .emoji: + namespace = Namespaces.ItemCollection.CloudEmojiPacks + } + let _ = (component.stickerContent.context.engine.stickers.reorderStickerPacks(namespace: namespace, itemIds: currentIds) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + }) + } } public func makeView() -> View { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift index 60d71d5d6d..ba093de32b 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift @@ -67,8 +67,6 @@ final class EntityKeyboardTopContainerPanelComponent: Component { private var visibilityFraction: CGFloat = 1.0 - private var hasExpandedPanels: Bool = false - override init(frame: CGRect) { super.init(frame: frame) @@ -84,6 +82,8 @@ 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 @@ -133,6 +133,10 @@ final class EntityKeyboardTopContainerPanelComponent: Component { self.addSubview(panelView.view) } + if !isExpanded { + panelView.isExpanded = false + } + let panelId = panel.id let _ = panelView.view.update( transition: panelTransition, @@ -198,6 +202,9 @@ final class EntityKeyboardTopContainerPanelComponent: Component { guard let panelView = self.panelViews[id] else { return } + if panelView.isExpanded == isExpanded { + return + } panelView.isExpanded = isExpanded var hasExpanded = false @@ -207,12 +214,8 @@ final class EntityKeyboardTopContainerPanelComponent: Component { break } } - - if self.hasExpandedPanels != hasExpanded { - self.hasExpandedPanels = hasExpanded - - self.panelEnvironment?.isExpandedUpdated(self.hasExpandedPanels, transition) - } + + self.panelEnvironment?.isExpandedUpdated(hasExpanded, transition) } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index 6a685fcf26..c8ac47cfe5 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -11,6 +11,7 @@ import AnimationCache import MultiAnimationRenderer import AccountContext import MultilineTextComponent +import LottieAnimationComponent final class EntityKeyboardAnimationTopPanelComponent: Component { typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment @@ -94,14 +95,15 @@ final class EntityKeyboardAnimationTopPanelComponent: Component { if self.itemLayer == nil { let itemLayer = EmojiPagerContentComponent.View.ItemLayer( item: EmojiPagerContentComponent.Item( - emoji: "", file: component.file, - stickerPackItem: nil + staticEmoji: nil, + subgroupId: nil ), context: component.context, groupId: "topPanel", attemptSynchronousLoad: false, file: component.file, + staticEmoji: nil, cache: component.animationCache, renderer: component.animationRenderer, placeholderColor: .lightGray, @@ -323,30 +325,429 @@ final class EntityKeyboardIconTopPanelComponent: Component { } } +final class EntityKeyboardStaticStickersPanelComponent: Component { + typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment + + let theme: PresentationTheme + let pressed: (EmojiPagerContentComponent.StaticEmojiSegment) -> Void + + init( + theme: PresentationTheme, + pressed: @escaping (EmojiPagerContentComponent.StaticEmojiSegment) -> Void + ) { + self.theme = theme + self.pressed = pressed + } + + static func ==(lhs: EntityKeyboardStaticStickersPanelComponent, rhs: EntityKeyboardStaticStickersPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + + return true + } + + final class View: UIView, UIScrollViewDelegate { + private let scrollView: UIScrollView + private var visibleItemViews: [EmojiPagerContentComponent.StaticEmojiSegment: ComponentView] = [:] + + private var component: EntityKeyboardStaticStickersPanelComponent? + + private var ignoreScrolling: Bool = false + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + + super.init(frame: frame) + + self.scrollView.layer.anchorPoint = CGPoint() + self.scrollView.delaysContentTouches = false + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.clipsToBounds = true + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let scrollViewLocation = recognizer.location(in: self.scrollView) + for (id, itemView) in self.visibleItemViews { + if let view = itemView.view, view.frame.insetBy(dx: -4.0, dy: -4.0).contains(scrollViewLocation) { + self.component?.pressed(id) + break + } + } + } + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + self.updateVisibleItems(transition: .immediate, animateAppearingItems: true) + } + + private func updateVisibleItems(transition: Transition, animateAppearingItems: Bool) { + guard let component = self.component else { + return + } + + let _ = component + + var validItemIds = Set() + let visibleBounds = self.scrollView.bounds + + let componentHeight: CGFloat = 32.0 + + let items = EmojiPagerContentComponent.StaticEmojiSegment.allCases + let itemSize: CGFloat = 28.0 + let itemSpacing: CGFloat = 4.0 + let sideInset: CGFloat = 2.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)) + if visibleBounds.intersects(itemFrame) { + let item = items[i] + validItemIds.insert(item) + + let itemView: ComponentView + if let current = self.visibleItemViews[item] { + itemView = current + } else { + itemView = ComponentView() + self.visibleItemViews[item] = itemView + } + + let animationName: String + switch item { + case .people: + animationName = "emojicat_smiles" + case .animalsAndNature: + animationName = "emojicat_animals" + case .foodAndDrink: + animationName = "emojicat_food" + case .activityAndSport: + animationName = "emojicat_activity" + case .travelAndPlaces: + animationName = "emojicat_places" + case .objects: + animationName = "emojicat_objects" + case .symbols: + animationName = "emojicat_symbols" + case .flags: + animationName = "emojicat_flags" + } + + let _ = itemView.update( + transition: .immediate, + component: AnyComponent(LottieAnimationComponent( + animation: LottieAnimationComponent.AnimationItem( + name: animationName, + colors: ["__allcolors__": component.theme.chat.inputMediaPanel.panelIconColor], + mode: animateAppearingItems ? .animating(loop: false) : .still(position: .end) + ), + size: CGSize(width: itemSize, height: itemSize) + )), + environment: {}, + containerSize: CGSize(width: itemSize, height: itemSize) + ) + if let view = itemView.view { + if view.superview == nil { + self.scrollView.addSubview(view) + } + view.frame = itemFrame + } + } + } + + var removedItemIds: [EmojiPagerContentComponent.StaticEmojiSegment] = [] + for (id, itemView) in self.visibleItemViews { + if !validItemIds.contains(id) { + removedItemIds.append(id) + itemView.view?.removeFromSuperview() + } + } + for id in removedItemIds { + self.visibleItemViews.removeValue(forKey: id) + } + } + + func update(component: EntityKeyboardStaticStickersPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.layer.cornerRadius = availableSize.height / 2.0 + + let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value + + self.component = component + + let itemSize: CGFloat = 28.0 + let itemSpacing: CGFloat = 4.0 + let sideInset: CGFloat = 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.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) + + if !itemEnvironment.isHighlighted && self.scrollView.contentOffset.x != 0.0 { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + final class EntityKeyboardTopPanelItemEnvironment: Equatable { let isExpanded: Bool + let isHighlighted: Bool - init(isExpanded: Bool) { + init(isExpanded: Bool, isHighlighted: Bool) { self.isExpanded = isExpanded + self.isHighlighted = isHighlighted } static func ==(lhs: EntityKeyboardTopPanelItemEnvironment, rhs: EntityKeyboardTopPanelItemEnvironment) -> Bool { if lhs.isExpanded != rhs.isExpanded { return false } + if lhs.isHighlighted != rhs.isHighlighted { + return false + } return true } } +private final class ReorderGestureRecognizer: UIGestureRecognizer { + private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemView: ComponentHostView?) + private let willBegin: (CGPoint) -> Void + private let began: (ComponentHostView) -> Void + private let ended: () -> Void + private let moved: (CGFloat) -> Void + private let isActiveUpdated: (Bool) -> Void + + private var initialLocation: CGPoint? + private var longTapTimer: SwiftSignalKit.Timer? + private var longPressTimer: SwiftSignalKit.Timer? + + private var itemView: ComponentHostView? + + public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemView: ComponentHostView?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (ComponentHostView) -> Void, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void, isActiveUpdated: @escaping (Bool) -> Void) { + self.shouldBegin = shouldBegin + self.willBegin = willBegin + self.began = began + self.ended = ended + self.moved = moved + self.isActiveUpdated = isActiveUpdated + + super.init(target: nil, action: nil) + } + + deinit { + self.longTapTimer?.invalidate() + self.longPressTimer?.invalidate() + } + + private func startLongTapTimer() { + self.longTapTimer?.invalidate() + let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in + self?.longTapTimerFired() + }, queue: Queue.mainQueue()) + self.longTapTimer = longTapTimer + longTapTimer.start() + } + + private func stopLongTapTimer() { + self.itemView = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + } + + private func startLongPressTimer() { + self.longPressTimer?.invalidate() + let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in + self?.longPressTimerFired() + }, queue: Queue.mainQueue()) + self.longPressTimer = longPressTimer + longPressTimer.start() + } + + private func stopLongPressTimer() { + self.itemView = nil + self.longPressTimer?.invalidate() + self.longPressTimer = nil + } + + override public func reset() { + super.reset() + + self.itemView = nil + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + } + + private func longTapTimerFired() { + guard let location = self.initialLocation else { + return + } + + self.longTapTimer?.invalidate() + self.longTapTimer = nil + + self.willBegin(location) + } + + private func longPressTimerFired() { + guard let _ = self.initialLocation else { + return + } + + self.isActiveUpdated(true) + self.state = .began + self.longPressTimer?.invalidate() + self.longPressTimer = nil + self.longTapTimer?.invalidate() + self.longTapTimer = nil + if let itemView = self.itemView { + self.began(itemView) + } + self.isActiveUpdated(true) + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if self.numberOfTouches > 1 { + self.isActiveUpdated(false) + self.state = .failed + self.ended() + return + } + + if self.state == .possible { + if let location = touches.first?.location(in: self.view) { + let (allowed, requiresLongPress, itemView) = self.shouldBegin(location) + if allowed { + self.isActiveUpdated(true) + + self.itemView = itemView + self.initialLocation = location + if requiresLongPress { + self.startLongTapTimer() + self.startLongPressTimer() + } else { + self.state = .began + if let itemView = self.itemView { + self.began(itemView) + } + } + } else { + self.isActiveUpdated(false) + self.state = .failed + } + } else { + self.isActiveUpdated(false) + self.state = .failed + } + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + super.touchesEnded(touches, with: event) + + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.stopLongPressTimer() + self.isActiveUpdated(false) + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.isActiveUpdated(false) + self.ended() + self.state = .failed + } + } + + override public func touchesCancelled(_ touches: Set, with event: UIEvent) { + super.touchesCancelled(touches, with: event) + + self.initialLocation = nil + + self.stopLongTapTimer() + if self.longPressTimer != nil { + self.isActiveUpdated(false) + self.stopLongPressTimer() + self.state = .failed + } + if self.state == .began || self.state == .changed { + self.isActiveUpdated(false) + self.ended() + self.state = .failed + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + super.touchesMoved(touches, with: event) + + if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) { + self.state = .changed + let offset = location.x - initialLocation.x + self.moved(offset) + } else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil { + let touchLocation = touch.location(in: self.view) + let dX = touchLocation.x - initialTapLocation.x + let dY = touchLocation.y - initialTapLocation.y + + if dX * dX + dY * dY > 3.0 * 3.0 { + self.stopLongTapTimer() + self.stopLongPressTimer() + self.initialLocation = nil + self.isActiveUpdated(false) + self.state = .failed + } + } + } +} + final class EntityKeyboardTopPanelComponent: Component { typealias EnvironmentType = EntityKeyboardTopContainerPanelEnvironment final class Item: Equatable { let id: AnyHashable + let isReorderable: Bool let content: AnyComponent - init(id: AnyHashable, content: AnyComponent) { + init(id: AnyHashable, isReorderable: Bool, content: AnyComponent) { self.id = id + self.isReorderable = isReorderable self.content = content } @@ -354,6 +755,9 @@ final class EntityKeyboardTopPanelComponent: Component { if lhs.id != rhs.id { return false } + if lhs.isReorderable != rhs.isReorderable { + return false + } if lhs.content != rhs.content { return false } @@ -366,17 +770,20 @@ final class EntityKeyboardTopPanelComponent: Component { let items: [Item] let defaultActiveItemId: AnyHashable? let activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)> + let reorderItems: ([Item]) -> Void init( theme: PresentationTheme, items: [Item], defaultActiveItemId: AnyHashable? = nil, - activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)> + activeContentItemIdUpdated: ActionSlot<(AnyHashable, Transition)>, + reorderItems: @escaping ([Item]) -> Void ) { self.theme = theme self.items = items self.defaultActiveItemId = defaultActiveItemId self.activeContentItemIdUpdated = activeContentItemIdUpdated + self.reorderItems = reorderItems } static func ==(lhs: EntityKeyboardTopPanelComponent, rhs: EntityKeyboardTopPanelComponent) -> Bool { @@ -398,24 +805,79 @@ final class EntityKeyboardTopPanelComponent: Component { final class View: UIView, UIScrollViewDelegate { private struct ItemLayout { + struct ItemDescription { + var isStatic: Bool + var isStaticExpanded: Bool + } + + struct Item { + var frame: CGRect + var innerFrame: CGRect + } + let sideInset: CGFloat = 7.0 let itemSize: CGSize + let staticItemSize: CGSize + let staticExpandedItemSize: CGSize let innerItemSize: CGSize let itemSpacing: CGFloat = 15.0 - let itemCount: Int let contentSize: CGSize let isExpanded: Bool + let items: [Item] - init(itemCount: Int, isExpanded: Bool, height: CGFloat) { + init(isExpanded: Bool, height: CGFloat, items: [ItemDescription]) { 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.innerItemSize = self.isExpanded ? CGSize(width: 50.0, height: 62.0) : CGSize(width: 28.0, height: 28.0) - self.itemCount = itemCount - self.contentSize = CGSize(width: sideInset * 2.0 + CGFloat(itemCount) * self.itemSize.width + CGFloat(max(0, itemCount - 1)) * itemSpacing, height: height) + + var contentSize = CGSize(width: sideInset, height: height) + var resultItems: [Item] = [] + + var isFirst = true + let itemY = floor((contentSize.height - self.itemSize.height) / 2.0) + for item in items { + if isFirst { + isFirst = false + } else { + contentSize.width += itemSpacing + } + let currentItemSize: CGSize + if item.isStaticExpanded { + currentItemSize = self.staticExpandedItemSize + } else if item.isStatic { + currentItemSize = self.staticItemSize + } else { + currentItemSize = self.itemSize + } + let frame = CGRect(origin: CGPoint(x: contentSize.width, y: itemY), size: currentItemSize) + + var innerFrame = frame + if item.isStaticExpanded { + } else if item.isStatic { + } else { + innerFrame.origin.x += floor((self.itemSize.width - self.innerItemSize.width)) / 2.0 + innerFrame.origin.y += floor((self.itemSize.height - self.innerItemSize.height)) / 2.0 + innerFrame.size = self.innerItemSize + } + + resultItems.append(Item( + frame: frame, + innerFrame: innerFrame + )) + + contentSize.width += frame.width + } + + contentSize.width += sideInset + + self.contentSize = contentSize + self.items = resultItems } func containerFrame(at index: Int) -> CGRect { - return CGRect(origin: CGPoint(x: sideInset + CGFloat(index) * (self.itemSize.width + self.itemSpacing), y: floor((self.contentSize.height - self.itemSize.height) / 2.0)), size: self.itemSize) + return self.items[index].frame } func contentFrame(containerFrame: CGRect) -> CGRect { @@ -427,19 +889,21 @@ final class EntityKeyboardTopPanelComponent: Component { } func contentFrame(at index: Int) -> CGRect { - return self.contentFrame(containerFrame: self.containerFrame(at: index)) + return self.items[index].innerFrame } func visibleItemRange(for rect: CGRect) -> (minIndex: Int, maxIndex: Int) { - let offsetRect = rect.offsetBy(dx: -self.sideInset, dy: 0.0) - var minVisibleColumn = Int(floor((offsetRect.minX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing))) - minVisibleColumn = max(0, minVisibleColumn) - let maxVisibleColumn = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize.width + self.itemSpacing))) - - let minVisibleIndex = minVisibleColumn - let maxVisibleIndex = min(maxVisibleColumn, self.itemCount - 1) - - return (minVisibleIndex, maxVisibleIndex) + for i in 0 ..< self.items.count { + if self.items[i].frame.intersects(rect) { + for j in i ..< self.items.count { + if !self.items[j].frame.intersects(rect) { + return (i, j - 1) + } + } + return (i, self.items.count - 1) + } + } + return (0, -1) } } @@ -447,10 +911,20 @@ final class EntityKeyboardTopPanelComponent: Component { private var itemViews: [AnyHashable: ComponentHostView] = [:] private var highlightedIconBackgroundView: UIView + private var temporaryReorderingOrderIndex: (id: AnyHashable, index: Int)? + + private weak var currentReorderingItemView: ComponentHostView? + private var currentReorderingItemId: AnyHashable? + private var currentReorderingItemContainerView: UIView? + private var initialReorderingItemFrame: CGRect? + private var itemLayout: ItemLayout? + private var items: [Item] = [] private var ignoreScrolling: Bool = false private var isDragging: Bool = false + private var isReordering: Bool = false + private var isDraggingOrReordering: Bool = false private var draggingStoppedTimer: SwiftSignalKit.Timer? private var isExpanded: Bool = false @@ -460,6 +934,7 @@ final class EntityKeyboardTopPanelComponent: Component { private var activeContentItemId: AnyHashable? private var component: EntityKeyboardTopPanelComponent? + weak var state: EmptyComponentState? private var environment: EntityKeyboardTopContainerPanelEnvironment? override init(frame: CGRect) { @@ -467,8 +942,8 @@ final class EntityKeyboardTopPanelComponent: Component { self.highlightedIconBackgroundView = UIView() self.highlightedIconBackgroundView.isUserInteractionEnabled = false - self.highlightedIconBackgroundView.layer.cornerRadius = 10.0 self.highlightedIconBackgroundView.clipsToBounds = true + self.highlightedIconBackgroundView.isHidden = true super.init(frame: frame) @@ -483,6 +958,7 @@ final class EntityKeyboardTopPanelComponent: Component { } self.scrollView.showsVerticalScrollIndicator = false self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = true self.scrollView.delegate = self self.addSubview(self.scrollView) @@ -496,6 +972,50 @@ final class EntityKeyboardTopPanelComponent: Component { } return strongSelf.scrollView.contentOffset.x > 0.0 } + + self.addGestureRecognizer(ReorderGestureRecognizer( + shouldBegin: { [weak self] point in + guard let strongSelf = self else { + return (false, false, nil) + } + if !strongSelf.isExpanded { + return (false, false, nil) + } + let scrollViewLocation = strongSelf.convert(point, to: strongSelf.scrollView) + for (id, itemView) in strongSelf.itemViews { + if itemView.frame.contains(scrollViewLocation) { + for item in strongSelf.items { + if item.id == id, item.isReorderable { + return (true, true, itemView) + } + } + break + } + } + return (false, false, nil) + }, willBegin: { _ in + }, began: { [weak self] itemView in + guard let strongSelf = self else { + return + } + strongSelf.beginReordering(itemView: itemView) + }, ended: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.endReordering() + }, moved: { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.updateReordering(offset: value) + }, isActiveUpdated: { [weak self] isActive in + guard let strongSelf = self else { + return + } + strongSelf.updateIsReordering(isActive) + } + )) } required init?(coder: NSCoder) { @@ -525,8 +1045,20 @@ final class EntityKeyboardTopPanelComponent: Component { } private func updateIsDragging(_ isDragging: Bool) { - if !isDragging { - if !self.isDragging { + self.isDragging = isDragging + self.updateIsDraggingOrReordering() + } + + private func updateIsReordering(_ isReordering: Bool) { + self.isReordering = isReordering + self.updateIsDraggingOrReordering() + } + + private func updateIsDraggingOrReordering() { + let isDraggingOrReordering = self.isDragging || self.isReordering + + if !isDraggingOrReordering { + if !self.isDraggingOrReordering { return } @@ -536,7 +1068,7 @@ final class EntityKeyboardTopPanelComponent: Component { return } strongSelf.draggingStoppedTimer = nil - strongSelf.isDragging = false + strongSelf.isDraggingOrReordering = false guard let environment = strongSelf.environment else { return } @@ -548,8 +1080,8 @@ final class EntityKeyboardTopPanelComponent: Component { self.draggingStoppedTimer?.invalidate() self.draggingStoppedTimer = nil - if !self.isDragging { - self.isDragging = true + if !self.isDraggingOrReordering { + self.isDraggingOrReordering = true guard let environment = self.environment else { return @@ -559,8 +1091,91 @@ final class EntityKeyboardTopPanelComponent: Component { } } + private func beginReordering(itemView: ComponentHostView) { + if let currentReorderingItemView = self.currentReorderingItemView { + if let componentView = currentReorderingItemView.componentView { + currentReorderingItemView.addSubview(componentView) + } + self.currentReorderingItemView = nil + self.currentReorderingItemId = nil + } + + guard let id = self.itemViews.first(where: { $0.value === itemView })?.key else { + return + } + + self.currentReorderingItemId = id + self.currentReorderingItemView = itemView + + let reorderingItemContainerView: UIView + if let current = self.currentReorderingItemContainerView { + reorderingItemContainerView = current + } else { + reorderingItemContainerView = UIView() + self.addSubview(reorderingItemContainerView) + self.currentReorderingItemContainerView = reorderingItemContainerView + } + + reorderingItemContainerView.alpha = 0.5 + reorderingItemContainerView.layer.animateAlpha(from: 1.0, to: 0.5, duration: 0.2) + + reorderingItemContainerView.frame = itemView.convert(itemView.bounds, to: self) + self.initialReorderingItemFrame = reorderingItemContainerView.frame + if let componentView = itemView.componentView { + reorderingItemContainerView.addSubview(componentView) + } + } + + private func endReordering() { + if let currentReorderingItemView = self.currentReorderingItemView { + self.currentReorderingItemView = nil + + if let componentView = currentReorderingItemView.componentView { + let localFrame = componentView.convert(componentView.bounds, to: self.scrollView) + currentReorderingItemView.superview?.bringSubviewToFront(currentReorderingItemView) + currentReorderingItemView.addSubview(componentView) + + let deltaPosition = CGPoint(x: localFrame.minX - currentReorderingItemView.frame.minX, y: localFrame.minY - currentReorderingItemView.frame.minY) + currentReorderingItemView.layer.animatePosition(from: deltaPosition, to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + } + } + + if let reorderingItemContainerView = self.currentReorderingItemContainerView { + self.currentReorderingItemContainerView = nil + reorderingItemContainerView.removeFromSuperview() + } + + self.currentReorderingItemId = nil + self.temporaryReorderingOrderIndex = nil + + self.component?.reorderItems(self.items) + //self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + } + + private func updateReordering(offset: CGFloat) { + guard let itemLayout = self.itemLayout, let currentReorderingItemId = self.currentReorderingItemId, let reorderingItemContainerView = self.currentReorderingItemContainerView, let initialReorderingItemFrame = self.initialReorderingItemFrame else { + return + } + reorderingItemContainerView.frame = initialReorderingItemFrame.offsetBy(dx: offset, dy: 0.0) + + for i in 0 ..< self.items.count { + if !self.items[i].isReorderable { + continue + } + let containerFrame = itemLayout.containerFrame(at: i) + if containerFrame.intersects(reorderingItemContainerView.frame) { + let temporaryReorderingOrderIndex: (id: AnyHashable, index: Int) = (currentReorderingItemId, i) + if self.temporaryReorderingOrderIndex?.id != temporaryReorderingOrderIndex.id || self.temporaryReorderingOrderIndex?.index != temporaryReorderingOrderIndex.index { + self.temporaryReorderingOrderIndex = temporaryReorderingOrderIndex + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring))) + } + break + } + } + } + private func updateVisibleItems(attemptSynchronousLoads: Bool, transition: Transition) { - guard let component = self.component, let itemLayout = self.itemLayout else { + guard let itemLayout = self.itemLayout else { return } @@ -569,9 +1184,9 @@ final class EntityKeyboardTopPanelComponent: Component { var validIds = Set() let visibleItemRange = itemLayout.visibleItemRange(for: visibleBounds) - if !component.items.isEmpty && visibleItemRange.maxIndex >= visibleItemRange.minIndex { + if !self.items.isEmpty && visibleItemRange.maxIndex >= visibleItemRange.minIndex { for index in visibleItemRange.minIndex ... visibleItemRange.maxIndex { - let item = component.items[index] + let item = self.items[index] validIds.insert(item.id) var itemTransition = transition @@ -590,7 +1205,7 @@ final class EntityKeyboardTopPanelComponent: Component { transition: itemTransition, component: item.content, environment: { - EntityKeyboardTopPanelItemEnvironment(isExpanded: itemLayout.isExpanded) + EntityKeyboardTopPanelItemEnvironment(isExpanded: itemLayout.isExpanded, isHighlighted: self.activeContentItemId == item.id) }, containerSize: itemOuterFrame.size ) @@ -618,6 +1233,7 @@ final class EntityKeyboardTopPanelComponent: Component { self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor } self.component = component + self.state = state if let defaultActiveItemId = component.defaultActiveItemId { self.activeContentItemId = defaultActiveItemId @@ -630,11 +1246,49 @@ final class EntityKeyboardTopPanelComponent: Component { let wasExpanded = self.isExpanded self.isExpanded = isExpanded + if !isExpanded { + if self.isDragging { + self.isDragging = false + } + if self.isReordering { + self.isReordering = false + } + if self.isDraggingOrReordering { + self.isDraggingOrReordering = false + } + if let draggingStoppedTimer = self.draggingStoppedTimer { + self.draggingStoppedTimer = nil + draggingStoppedTimer.invalidate() + } + } + let intrinsicHeight: CGFloat = availableSize.height let height = intrinsicHeight + var items = component.items + if let (id, index) = self.temporaryReorderingOrderIndex { + for i in 0 ..< items.count { + if items[i].id == id { + let item = items.remove(at: i) + items.insert(item, at: min(index, items.count)) + break + } + } + } + self.items = items + + if self.activeContentItemId == nil { + self.activeContentItemId = items.first?.id + } + let previousItemLayout = self.itemLayout - let itemLayout = ItemLayout(itemCount: component.items.count, isExpanded: isExpanded, height: availableSize.height) + let itemLayout = ItemLayout(isExpanded: isExpanded, height: availableSize.height, items: self.items.map { item -> ItemLayout.ItemDescription in + let isStatic = item.id == AnyHashable("static") + return ItemLayout.ItemDescription( + isStatic: isStatic, + isStaticExpanded: isStatic && self.activeContentItemId == item.id + ) + }) self.itemLayout = itemLayout self.ignoreScrolling = true @@ -660,7 +1314,7 @@ final class EntityKeyboardTopPanelComponent: Component { 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[component.items[index].id] { + 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) @@ -685,11 +1339,23 @@ final class EntityKeyboardTopPanelComponent: Component { self.updateVisibleItems(attemptSynchronousLoads: !(self.scrollView.isDragging || self.scrollView.isDecelerating), transition: transition) if let activeContentItemId = self.activeContentItemId { - if let index = component.items.firstIndex(where: { $0.id == activeContentItemId }) { + if let index = self.items.firstIndex(where: { $0.id == activeContentItemId }) { let itemFrame = itemLayout.containerFrame(at: index) - transition.setPosition(view: self.highlightedIconBackgroundView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY)) - transition.setBounds(view: self.highlightedIconBackgroundView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + + var highlightTransition = transition + if self.highlightedIconBackgroundView.isHidden { + self.highlightedIconBackgroundView.isHidden = false + highlightTransition = .immediate + } + + highlightTransition.setCornerRadius(layer: self.highlightedIconBackgroundView.layer, cornerRadius: activeContentItemId.base is String ? 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 { + self.highlightedIconBackgroundView.isHidden = true } + } else { + self.highlightedIconBackgroundView.isHidden = true } transition.setAlpha(view: self.highlightedIconBackgroundView, alpha: isExpanded ? 0.0 : 1.0) @@ -732,17 +1398,28 @@ final class EntityKeyboardTopPanelComponent: Component { guard let component = self.component, let itemLayout = self.itemLayout else { return } - + if self.activeContentItemId == itemId { + return + } self.activeContentItemId = itemId + + let _ = component + let _ = itemLayout + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) - var found = false - for i in 0 ..< component.items.count { - if component.items[i].id == itemId { + /*var found = false + for i in 0 ..< self.items.count { + if self.items[i].id == itemId { found = true self.highlightedIconBackgroundView.isHidden = false let itemFrame = itemLayout.containerFrame(at: i) - transition.setPosition(view: self.highlightedIconBackgroundView, position: CGPoint(x: itemFrame.midX, y: itemFrame.midY)) - transition.setBounds(view: self.highlightedIconBackgroundView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) + + var highlightTransition = transition + if highlightTransition.animation.isImmediate { + highlightTransition = highlightTransition.withAnimation(.curve(duration: 0.3, curve: .spring)) + } + 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)) self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: -6.0, dy: 0.0), animated: true) @@ -751,7 +1428,7 @@ final class EntityKeyboardTopPanelComponent: Component { } if !found { self.highlightedIconBackgroundView.isHidden = true - } + }*/ } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index c57bb93a13..b18ac64c11 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -18,6 +18,7 @@ import PagerComponent import SoftwareVideo import AVFoundation import PhotoResources +import ContextUI private class GifVideoLayer: AVSampleBufferDisplayLayer { private let context: AccountContext @@ -125,11 +126,14 @@ public final class GifPagerContentComponent: Component { public final class InputInteraction { public let performItemAction: (Item, UIView, CGRect) -> Void + public let openGifContextMenu: (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void public init( - performItemAction: @escaping (Item, UIView, CGRect) -> Void + performItemAction: @escaping (Item, UIView, CGRect) -> Void, + openGifContextMenu: @escaping (TelegramMediaFile, UIView, CGRect, ContextGesture, Bool) -> Void ) { self.performItemAction = performItemAction + self.openGifContextMenu = openGifContextMenu } } @@ -186,7 +190,7 @@ public final class GifPagerContentComponent: Component { return true } - public final class View: UIView, UIScrollViewDelegate { + public final class View: ContextControllerSourceView, UIScrollViewDelegate { private struct ItemGroupDescription: Equatable { let hasTitle: Bool let itemCount: Int @@ -406,12 +410,44 @@ public final class GifPagerContentComponent: Component { self.addSubview(self.scrollView) self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + + self.useSublayerTransformForActivation = false + self.shouldBegin = { [weak self] point in + guard let strongSelf = self else { + return false + } + strongSelf.targetLayerForActivationProgress = nil + if let (_, itemLayer) = strongSelf.itemLayer(atPoint: point) { + strongSelf.targetLayerForActivationProgress = itemLayer + return true + } + return false + } + self.activated = { [weak self] gesture, location in + guard let strongSelf = self, let component = strongSelf.component else { + gesture.cancel() + return + } + guard let (item, itemLayer) = strongSelf.itemLayer(atPoint: location) else { + gesture.cancel() + return + } + let rect = strongSelf.scrollView.convert(itemLayer.frame, to: strongSelf) + component.inputInteraction.openGifContextMenu(item.file, strongSelf, rect, gesture, true) + } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + private func openGifContextMenu(file: TelegramMediaFile, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { + guard let component = self.component else { + return + } + component.inputInteraction.openGifContextMenu(file, sourceView, sourceRect, gesture, isSaved) + } + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[item.file.fileId] { @@ -432,6 +468,18 @@ public final class GifPagerContentComponent: Component { return nil } + private func itemLayer(atPoint point: CGPoint) -> (Item, ItemLayer)? { + let localPoint = self.convert(point, to: self.scrollView) + + for (_, itemLayer) in self.visibleItemLayers { + if itemLayer.frame.contains(localPoint) { + return (itemLayer.item, itemLayer) + } + } + + return nil + } + private var previousScrollingOffset: CGFloat? public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { @@ -600,4 +648,3 @@ public final class GifPagerContentComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } - diff --git a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift index a3f4e96a63..0a738c851f 100644 --- a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift +++ b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift @@ -26,3 +26,9 @@ public func cacheLottieAnimation(data: Data, width: Int, height: Int, writer: An writer.finish() } } + +public func cacheStillSticker(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter) { + writer.queue.async { + writer.finish() + } +} diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD b/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD index e5a853e167..4f5a66267d 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/BUILD @@ -1,4 +1,44 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") +load( + "@build_bazel_rules_apple//apple:resources.bzl", + "apple_resource_bundle", + "apple_resource_group", +) +load("//build-system/bazel-utils:plist_fragment.bzl", + "plist_fragment", +) + +filegroup( + name = "MultiAnimationRendererMetalResources", + srcs = glob([ + "Resources/**/*.metal", + ]), + visibility = ["//visibility:public"], +) + +plist_fragment( + name = "WallpaperBackgroundNodeBundleInfoPlist", + extension = "plist", + template = + """ + CFBundleIdentifier + org.telegram.MultiAnimationRenderer + CFBundleDevelopmentRegion + en + CFBundleName + MultiAnimationRenderer + """ +) + +apple_resource_bundle( + name = "MultiAnimationRendererBundle", + infoplists = [ + ":WallpaperBackgroundNodeBundleInfoPlist", + ], + resources = [ + ":MultiAnimationRendererMetalResources", + ], +) swift_library( name = "MultiAnimationRenderer", @@ -6,6 +46,9 @@ swift_library( srcs = glob([ "Sources/**/*.swift", ]), + data = [ + ":MultiAnimationRendererBundle", + ], copts = [ "-warnings-as-errors", ], diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal b/submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal new file mode 100644 index 0000000000..35a1806d35 --- /dev/null +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Resources/MultiAnimationRendererShaders.metal @@ -0,0 +1,38 @@ +#include +using namespace metal; + +typedef struct { + packed_float2 position; + packed_float2 texCoord; +} Vertex; + +typedef struct { + float4 position[[position]]; + float2 texCoord; +} Varyings; + +vertex Varyings multiAnimationVertex( + unsigned int vid[[vertex_id]], + constant Vertex *verticies[[buffer(0)]], + constant uint2 &resolution[[buffer(1)]], + constant uint2 &slotSize[[buffer(2)]], + constant uint2 &slotPosition[[buffer(3)]] +) { + Varyings out; + constant Vertex &v = verticies[vid]; + + + + out.position = float4(float2(v.position), 0.0, 1.0); + out.texCoord = v.texCoord; + + return out; +} + +fragment half4 multiAnimationFragment( + Varyings in[[stage_in]], + texture2d texture[[texture(0)]] +) { + constexpr sampler s(address::clamp_to_edge, filter::linear); + return half4(texture.sample(s, in.texCoord)); +} diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift new file mode 100644 index 0000000000..2a3646512d --- /dev/null +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationMetalRenderer.swift @@ -0,0 +1,767 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import AnimationCache +import Accelerate +import simd + +private func alignUp(size: Int, align: Int) -> Int { + precondition(((align - 1) & align) == 0, "Align must be a power of two") + + let alignmentMask = align - 1 + return (size + alignmentMask) & ~alignmentMask +} + +private extension Float { + func remap(fromLow: Float, fromHigh: Float, toLow: Float, toHigh: Float) -> Float { + guard (fromHigh - fromLow) != 0.0 else { + return 0.0 + } + return toLow + (self - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + } +} + +private func makePipelineState(device: MTLDevice, library: MTLLibrary, vertexProgram: String, fragmentProgram: String) -> MTLRenderPipelineState? { + guard let loadedVertexProgram = library.makeFunction(name: vertexProgram) else { + return nil + } + guard let loadedFragmentProgram = library.makeFunction(name: fragmentProgram) else { + return nil + } + + let pipelineStateDescriptor = MTLRenderPipelineDescriptor() + pipelineStateDescriptor.vertexFunction = loadedVertexProgram + pipelineStateDescriptor.fragmentFunction = loadedFragmentProgram + pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineStateDescriptor) else { + return nil + } + + return pipelineState +} + +@available(iOS 13.0, *) +public final class MultiAnimationMetalRendererImpl: MultiAnimationRenderer { + private final class LoadFrameTask { + let task: () -> () -> Void + + init(task: @escaping () -> () -> Void) { + self.task = task + } + } + + private final class TargetReference { + let id: Int64 + weak var value: MultiAnimationRenderTarget? + + init(_ value: MultiAnimationRenderTarget) { + self.value = value + self.id = value.id + } + } + + private final class TextureStoragePool { + let width: Int + let height: Int + + private var items: [TextureStorage.Content] = [] + + init(width: Int, height: Int) { + self.width = width + self.height = height + } + + func recycle(content: TextureStorage.Content) { + if self.items.count < 4 { + self.items.append(content) + } else { + print("Warning: over-recycling texture storage") + } + } + + func take(device: MTLDevice) -> TextureStorage.Content? { + if self.items.isEmpty { + guard let content = TextureStorage.Content(device: device, width: self.width, height: self.height) else { + return nil + } + return content + } + return self.items.removeLast() + } + } + + private final class TextureStorage { + final class Content { + let buffer: MTLBuffer? + + let width: Int + let height: Int + let bytesPerRow: Int + let texture: MTLTexture + + init?(device: MTLDevice, width: Int, height: Int) { + let bytesPerPixel = 4 + let pixelRowAlignment = device.minimumLinearTextureAlignment(for: .bgra8Unorm) + let bytesPerRow = alignUp(size: width * bytesPerPixel, align: pixelRowAlignment) + + self.width = width + self.height = height + self.bytesPerRow = bytesPerRow + + #if targetEnvironment(simulator) + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.pixelFormat = .bgra8Unorm + textureDescriptor.width = width + textureDescriptor.height = height + textureDescriptor.usage = [.renderTarget] + textureDescriptor.storageMode = .shared + + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + return nil + } + self.buffer = nil + #else + guard let buffer = device.makeBuffer(length: bytesPerRow * height, options: MTLResourceOptions.storageModeShared) else { + return nil + } + self.buffer = buffer + + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.pixelFormat = .bgra8Unorm + textureDescriptor.width = width + textureDescriptor.height = height + textureDescriptor.usage = [.renderTarget] + textureDescriptor.storageMode = buffer.storageMode + + guard let texture = buffer.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: bytesPerRow) else { + return nil + } + #endif + + self.texture = texture + } + + func replace(rgbaData: Data, range: Range, width: Int, height: Int, bytesPerRow: Int) { + if width != self.width || height != self.height { + assert(false, "Image size does not match") + return + } + let region = MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), size: MTLSize(width: width, height: height, depth: 1)) + + if let buffer = self.buffer, self.bytesPerRow == bytesPerRow { + rgbaData.withUnsafeBytes { bytes in + let _ = memcpy(buffer.contents(), bytes.baseAddress!.advanced(by: range.lowerBound), bytesPerRow * height) + } + } else { + rgbaData.withUnsafeBytes { bytes in + self.texture.replace(region: region, mipmapLevel: 0, withBytes: bytes.baseAddress!.advanced(by: range.lowerBound), bytesPerRow: bytesPerRow) + } + } + } + } + + private weak var pool: TextureStoragePool? + let content: Content + private var isInvalidated: Bool = false + + init(pool: TextureStoragePool, content: Content) { + self.pool = pool + self.content = content + } + + deinit { + if !self.isInvalidated { + self.pool?.recycle(content: self.content) + } + } + + /*func createCGImage() -> CGImage? { + if self.isInvalidated { + return nil + } + self.isInvalidated = true + + #if targetEnvironment(simulator) + guard let data = NSMutableData(capacity: self.content.bytesPerRow * self.content.height) else { + return nil + } + data.length = self.content.bytesPerRow * self.content.height + self.content.texture.getBytes(data.mutableBytes, bytesPerRow: self.content.bytesPerRow, bytesPerImage: self.content.bytesPerRow * self.content.height, from: MTLRegion(origin: MTLOrigin(), size: MTLSize(width: self.content.width, height: self.content.height, depth: 1)), mipmapLevel: 0, slice: 0) + + guard let dataProvider = CGDataProvider(data: data as CFData) else { + return nil + } + #else + let content = self.content + let pool = self.pool + guard let dataProvider = CGDataProvider(data: Data(bytesNoCopy: self.content.buffer.contents(), count: self.content.buffer.length, deallocator: .custom { [weak pool] _, _ in + guard let pool = pool else { + return + } + pool.recycle(content: content) + }) as CFData) else { + return nil + } + #endif + + guard let image = CGImage( + width: Int(self.content.width), + height: Int(self.content.height), + bitsPerComponent: 8, + bitsPerPixel: 8 * 4, + bytesPerRow: self.content.bytesPerRow, + space: DeviceGraphicsContextSettings.shared.colorSpace, + bitmapInfo: DeviceGraphicsContextSettings.shared.transparentBitmapInfo, + provider: dataProvider, + decode: nil, + shouldInterpolate: true, + intent: .defaultIntent + ) else { + return nil + } + + return image + }*/ + } + + private final class Frame { + let timestamp: Double + let texture: TextureStorage.Content + + init(device: MTLDevice, texture: TextureStorage.Content, data: AnimationCacheItemFrame, timestamp: Double) { + self.timestamp = timestamp + self.texture = texture + + switch data.format { + case let .rgba(width, height, bytesPerRow): + texture.replace(rgbaData: data.data, range: data.range, width: width, height: height, bytesPerRow: bytesPerRow) + } + } + } + + private final class ItemContext { + static let queue = Queue(name: "MultiAnimationMetalRendererImpl", qos: .default) + + private let cache: AnimationCache + private let stateUpdated: () -> Void + + private var disposable: Disposable? + private var timestamp: Double = 0.0 + private var item: AnimationCacheItem? + + private(set) var currentFrame: Frame? + private var isLoadingFrame: Bool = false + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + self.stateUpdated() + } + } + } + + var targets: [TargetReference] = [] + var slotIndex: Int + + init(slotIndex: Int, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) { + self.slotIndex = slotIndex + self.cache = cache + self.stateUpdated = stateUpdated + + self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + strongSelf.item = result.item + strongSelf.updateIsPlaying() + + if result.item == nil { + for target in strongSelf.targets { + if let target = target.value { + target.updateDisplayPlaceholder(displayPlaceholder: true) + } + } + } + } + }) + } + + deinit { + self.disposable?.dispose() + } + + func updateIsPlaying() { + var isPlaying = true + if self.item == nil { + isPlaying = false + } + + var shouldBeAnimating = false + for target in self.targets { + if let target = target.value { + if target.shouldBeAnimating { + shouldBeAnimating = true + break + } + } + } + if !shouldBeAnimating { + isPlaying = false + } + + self.isPlaying = isPlaying + } + + func animationTick(device: MTLDevice, texturePool: TextureStoragePool, advanceTimestamp: Double) -> LoadFrameTask? { + return self.update(device: device, texturePool: texturePool, advanceTimestamp: advanceTimestamp) + } + + private func update(device: MTLDevice, texturePool: 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, currentFrame.timestamp == self.timestamp { + } else if !self.isLoadingFrame { + self.isLoadingFrame = true + + return LoadFrameTask(task: { [weak self] in + let frame = item.getFrame(at: timestamp) + + return { + guard let strongSelf = self else { + return + } + + var currentFrame: Frame? + let texture = texturePool.take(device: device) + if let frame = frame, let texture = texture { + currentFrame = Frame(device: device, texture: texture, data: frame, timestamp: timestamp) + } + + strongSelf.isLoadingFrame = false + + if let currentFrame = currentFrame { + strongSelf.currentFrame = currentFrame + } + } + }) + } + + return nil + } + } + + private final class SurfaceLayer: CAMetalLayer { + private let cellSize: CGSize + private let stateUpdated: () -> Void + + private let metalDevice: MTLDevice + private let commandQueue: MTLCommandQueue + private let renderPipelineState: MTLRenderPipelineState + + private let texturePool: TextureStoragePool + + private let slotCount: Int + private let slotsX: Int + private let slotsY: Int + private var itemContexts: [String: ItemContext] = [:] + private var slotToItemId: [String?] + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + self.stateUpdated() + } + } + } + + public init(cellSize: CGSize, stateUpdated: @escaping () -> Void) { + self.cellSize = cellSize + self.stateUpdated = stateUpdated + + self.slotsX = 16 + self.slotsY = 16 + let drawableSize = CGSize(width: cellSize.width * CGFloat(self.slotsX), height: cellSize.height * CGFloat(self.slotsY)) + + self.slotCount = (Int(drawableSize.width) / Int(cellSize.width)) * (Int(drawableSize.height) / Int(cellSize.height)) + self.slotToItemId = (0 ..< self.slotCount).map { _ in nil } + + self.metalDevice = MTLCreateSystemDefaultDevice()! + self.commandQueue = self.metalDevice.makeCommandQueue()! + + let mainBundle = Bundle(for: MultiAnimationMetalRendererImpl.self) + + guard let path = mainBundle.path(forResource: "MultiAnimationRendererBundle", ofType: "bundle") else { + preconditionFailure() + } + guard let bundle = Bundle(path: path) else { + preconditionFailure() + } + guard let defaultLibrary = try? self.metalDevice.makeDefaultLibrary(bundle: bundle) else { + preconditionFailure() + } + + self.renderPipelineState = makePipelineState(device: self.metalDevice, library: defaultLibrary, vertexProgram: "multiAnimationVertex", fragmentProgram: "multiAnimationFragment")! + + self.texturePool = TextureStoragePool(width: Int(self.cellSize.width), height: Int(self.cellSize.height)) + + super.init() + + self.device = self.metalDevice + self.maximumDrawableCount = 2 + //self.metalLayer.presentsWithTransaction = true + self.contentsScale = 1.0 + + self.drawableSize = drawableSize + + self.pixelFormat = .bgra8Unorm + self.framebufferOnly = true + self.allowsNextDrawableTimeout = true + } + + override public init(layer: Any) { + preconditionFailure() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func action(forKey event: String) -> CAAction? { + return nullAction + } + + func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable? { + if size != self.cellSize { + return nil + } + + let targetId = target.id + + if self.itemContexts[itemId] == nil { + for i in 0 ..< self.slotCount { + if self.slotToItemId[i] == nil { + self.slotToItemId[i] = itemId + self.itemContexts[itemId] = ItemContext(slotIndex: i, cache: cache, itemId: itemId, size: size, fetch: fetch, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + break + } + } + } + + if let itemContext = self.itemContexts[itemId] { + itemContext.targets.append(TargetReference(target)) + target.contents = self.contents + + let slotX = itemContext.slotIndex % self.slotsX + let slotY = itemContext.slotIndex / self.slotsX + let totalX = CGFloat(self.slotsX) * self.cellSize.width + let totalY = CGFloat(self.slotsY) * self.cellSize.height + let contentsRect = CGRect(origin: CGPoint(x: (CGFloat(slotX) * self.cellSize.width) / totalX, y: (CGFloat(slotY) * self.cellSize.height) / totalY), size: CGSize(width: self.cellSize.width / totalX, height: self.cellSize.height / totalY)) + target.contentsRect = contentsRect + + self.isPlaying = true + + return ActionDisposable { [weak self, weak itemContext] in + Queue.mainQueue().async { + guard let strongSelf = self, let currentItemContext = strongSelf.itemContexts[itemId], currentItemContext === itemContext else { + return + } + if let index = currentItemContext.targets.firstIndex(where: { $0.id == targetId }) { + currentItemContext.targets.remove(at: index) + if currentItemContext.targets.isEmpty { + strongSelf.slotToItemId[currentItemContext.slotIndex] = nil + strongSelf.itemContexts.removeValue(forKey: itemId) + + if strongSelf.itemContexts.isEmpty { + strongSelf.isPlaying = false + } + } + } + } + } + } else { + return nil + } + } + + private func updateIsPlaying() { + var isPlaying = false + for (_, itemContext) in self.itemContexts { + if itemContext.isPlaying { + isPlaying = true + break + } + } + + self.isPlaying = isPlaying + } + + func animationTick(advanceTimestamp: Double) -> [LoadFrameTask] { + var tasks: [LoadFrameTask] = [] + for (_, itemContext) in self.itemContexts { + if itemContext.isPlaying { + if let task = itemContext.animationTick(device: self.metalDevice, texturePool: self.texturePool, advanceTimestamp: advanceTimestamp) { + tasks.append(task) + } + } + } + + return tasks + } + + func redraw() { + guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { + return + } + guard let drawable = self.nextDrawable() else { + return + } + + /*let drawTime = CACurrentMediaTime() - timestamp + if drawTime > 9.0 / 1000.0 { + print("get time \(drawTime * 1000.0)") + }*/ + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = drawable.texture + renderPassDescriptor.colorAttachments[0].loadAction = .clear + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor( + red: 0.0, + green: 0.0, + blue: 0.0, + alpha: 0.0 + ) + + guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return + } + + var usedTextures: [MultiAnimationMetalRendererImpl.TextureStorage.Content] = [] + + var vertices: [Float] = [ + -1.0, -1.0, 0.0, 0.0, + 1.0, -1.0, 1.0, 0.0, + -1.0, 1.0, 0.0, 1.0, + 1.0, 1.0, 1.0, 1.0 + ] + + renderEncoder.setRenderPipelineState(self.renderPipelineState) + + var resolution = simd_uint2(UInt32(drawable.texture.width), UInt32(drawable.texture.height)) + renderEncoder.setVertexBytes(&resolution, length: MemoryLayout.size * 2, index: 1) + + var slotSize = simd_uint2(UInt32(self.cellSize.width), UInt32(self.cellSize.height)) + renderEncoder.setVertexBytes(&slotSize, length: MemoryLayout.size * 2, index: 2) + + for (_, itemContext) in self.itemContexts { + guard let frame = itemContext.currentFrame else { + continue + } + + let slotX = itemContext.slotIndex % self.slotsX + let slotY = self.slotsY - 1 - itemContext.slotIndex / self.slotsY + let totalX = CGFloat(self.slotsX) * self.cellSize.width + let totalY = CGFloat(self.slotsY) * self.cellSize.height + + let contentsRect = CGRect(origin: CGPoint(x: (CGFloat(slotX) * self.cellSize.width) / totalX, y: (CGFloat(slotY) * self.cellSize.height) / totalY), size: CGSize(width: self.cellSize.width / totalX, height: self.cellSize.height / totalY)) + + vertices[4 * 0 + 0] = Float(contentsRect.minX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) + vertices[4 * 0 + 1] = Float(contentsRect.minY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) + + vertices[4 * 1 + 0] = Float(contentsRect.maxX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) + vertices[4 * 1 + 1] = Float(contentsRect.minY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) + + vertices[4 * 2 + 0] = Float(contentsRect.minX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) + vertices[4 * 2 + 1] = Float(contentsRect.maxY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) + + vertices[4 * 3 + 0] = Float(contentsRect.maxX).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) + vertices[4 * 3 + 1] = Float(contentsRect.maxY).remap(fromLow: 0.0, fromHigh: 1.0, toLow: -1.0, toHigh: 1.0) + + renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0) + + var slotPosition = simd_uint2(UInt32(itemContext.slotIndex % self.slotsX), UInt32(itemContext.slotIndex % self.slotsY)) + renderEncoder.setVertexBytes(&slotPosition, length: MemoryLayout.size * 2, index: 3) + + usedTextures.append(frame.texture) + renderEncoder.setFragmentTexture(frame.texture.texture, index: 0) + + renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4, instanceCount: 1) + } + + renderEncoder.endEncoding() + + if self.presentsWithTransaction { + if Thread.isMainThread { + commandBuffer.commit() + commandBuffer.waitUntilScheduled() + drawable.present() + } else { + CATransaction.begin() + commandBuffer.commit() + commandBuffer.waitUntilScheduled() + drawable.present() + CATransaction.commit() + } + } else { + commandBuffer.addScheduledHandler { _ in + drawable.present() + } + commandBuffer.addCompletedHandler { _ in + DispatchQueue.main.async { + for _ in usedTextures { + } + } + } + commandBuffer.commit() + } + } + } + + private var nextSurfaceLayerIndex: Int = 1 + private var surfaceLayers: [Int: SurfaceLayer] = [:] + + private var frameSkip: Int + private var displayLink: ConstantDisplayLinkAnimator? + + private(set) var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + if self.isPlaying { + if self.displayLink == nil { + self.displayLink = ConstantDisplayLinkAnimator { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.animationTick() + } + self.displayLink?.frameInterval = self.frameSkip + self.displayLink?.isPaused = false + } + } else { + if let displayLink = self.displayLink { + self.displayLink = nil + displayLink.invalidate() + } + } + } + } + } + + public init() { + if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.activeProcessorCount > 2 { + self.frameSkip = 1 + } else { + self.frameSkip = 2 + } + } + + private func updateIsPlaying() { + var isPlaying = false + for (_, surfaceLayer) in self.surfaceLayers { + if surfaceLayer.isPlaying { + isPlaying = true + break + } + } + + self.isPlaying = isPlaying + } + + public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { + assert(Thread.isMainThread) + + let alignedSize = CGSize(width: CGFloat(alignUp(size: Int(size.width), align: 16)), height: CGFloat(alignUp(size: Int(size.height), align: 16))) + + for (_, surfaceLayer) in self.surfaceLayers { + if let disposable = surfaceLayer.add(target: target, cache: cache, itemId: itemId, size: alignedSize, fetch: fetch) { + return disposable + } + } + + let index = self.nextSurfaceLayerIndex + self.nextSurfaceLayerIndex += 1 + let surfaceLayer = SurfaceLayer(cellSize: alignedSize, stateUpdated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.updateIsPlaying() + }) + self.surfaceLayers[index] = surfaceLayer + if let disposable = surfaceLayer.add(target: target, cache: cache, itemId: itemId, size: alignedSize, fetch: fetch) { + return disposable + } else { + return EmptyDisposable + } + } + + public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + return false + } + + public func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable { + completion(false) + + return EmptyDisposable + } + + private func animationTick() { + let secondsPerFrame = Double(self.frameSkip) / 60.0 + + var tasks: [LoadFrameTask] = [] + var surfaceLayersWithTasks: [Int] = [] + for (index, surfaceLayer) in self.surfaceLayers { + var hasTasks = false + if surfaceLayer.isPlaying { + let surfaceLayerTasks = surfaceLayer.animationTick(advanceTimestamp: secondsPerFrame) + if !surfaceLayerTasks.isEmpty { + tasks.append(contentsOf: surfaceLayerTasks) + hasTasks = true + } + } + if hasTasks { + surfaceLayersWithTasks.append(index) + } + } + + if !tasks.isEmpty { + ItemContext.queue.async { [weak self] in + var completions: [() -> Void] = [] + for task in tasks { + let complete = task.task() + completions.append(complete) + } + + if !completions.isEmpty { + Queue.mainQueue().async { + for completion in completions { + completion() + } + } + } + + if let strongSelf = self { + for index in surfaceLayersWithTasks { + if let surfaceLayer = strongSelf.surfaceLayers[index] { + surfaceLayer.redraw() + } + } + } + } + } + } +} diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index 3354285be9..6b354caa28 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -11,7 +11,11 @@ public protocol MultiAnimationRenderer: AnyObject { func loadFirstFrame(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, completion: @escaping (Bool) -> Void) -> Disposable } +private var nextRenderTargetId: Int64 = 1 + open class MultiAnimationRenderTarget: SimpleLayer { + public let id: Int64 + fileprivate let deinitCallbacks = Bag<() -> Void>() fileprivate let updateStateCallbacks = Bag<() -> Void>() @@ -25,6 +29,29 @@ open class MultiAnimationRenderTarget: SimpleLayer { } } + public override init() { + assert(Thread.isMainThread) + + self.id = nextRenderTargetId + nextRenderTargetId += 1 + + super.init() + } + + public override init(layer: Any) { + guard let layer = layer as? MultiAnimationRenderTarget else { + preconditionFailure() + } + + self.id = layer.id + + super.init(layer: layer) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + deinit { for f in self.deinitCallbacks.copyItems() { f() @@ -369,7 +396,8 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } } - private let firstFrameQueue: Queue + public static let firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive) + private var groupContexts: [String: GroupContext] = [:] private var frameSkip: Int private var displayLink: ConstantDisplayLinkAnimator? @@ -399,8 +427,6 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } public init() { - self.firstFrameQueue = Queue(name: "MultiAnimationRenderer-FirstFrame", qos: .userInteractive) - if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.activeProcessorCount > 2 { self.frameSkip = 1 } else { @@ -413,7 +439,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { if let current = self.groupContexts[groupId] { groupContext = current } else { - groupContext = GroupContext(firstFrameQueue: self.firstFrameQueue, stateUpdated: { [weak self] in + groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in guard let strongSelf = self else { return } @@ -434,7 +460,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { if let current = self.groupContexts[groupId] { groupContext = current } else { - groupContext = GroupContext(firstFrameQueue: self.firstFrameQueue, stateUpdated: { [weak self] in + groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in guard let strongSelf = self else { return } @@ -451,7 +477,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { if let current = self.groupContexts[groupId] { groupContext = current } else { - groupContext = GroupContext(firstFrameQueue: self.firstFrameQueue, stateUpdated: { [weak self] in + groupContext = GroupContext(firstFrameQueue: MultiAnimationRendererImpl.firstFrameQueue, stateUpdated: { [weak self] in guard let strongSelf = self else { return } @@ -475,14 +501,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { self.isPlaying = isPlaying } - private var previousTimestamp: Double? - private func animationTick() { - let timestamp = CFAbsoluteTimeGetCurrent() - if let _ = self.previousTimestamp { - } - self.previousTimestamp = timestamp - let secondsPerFrame = Double(self.frameSkip) / 60.0 var tasks: [LoadFrameGroupTask] = [] diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 724c187b04..d0809698b0 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -45,17 +45,20 @@ public final class TextNodeWithEntities { public let cache: AnimationCache public let renderer: MultiAnimationRenderer public let placeholderColor: UIColor + public let attemptSynchronous: Bool public init( context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, - placeholderColor: UIColor + placeholderColor: UIColor, + attemptSynchronous: Bool ) { self.context = context self.cache = cache self.renderer = renderer self.placeholderColor = placeholderColor + self.attemptSynchronous = attemptSynchronous } } @@ -115,7 +118,7 @@ public final class TextNodeWithEntities { if let maybeNode = maybeNode { if let applyArguments = applyArguments { - maybeNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor) + maybeNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false) } return maybeNode @@ -123,7 +126,7 @@ public final class TextNodeWithEntities { let resultNode = TextNodeWithEntities(textNode: result) if let applyArguments = applyArguments { - resultNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor) + resultNode.updateInlineStickers(context: applyArguments.context, cache: applyArguments.cache, renderer: applyArguments.renderer, textLayout: layout, placeholderColor: applyArguments.placeholderColor, attemptSynchronousLoad: false) } return resultNode @@ -140,7 +143,7 @@ public final class TextNodeWithEntities { } } - private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor) { + private func updateInlineStickers(context: AccountContext, cache: AnimationCache, renderer: MultiAnimationRenderer, textLayout: TextNodeLayout?, placeholderColor: UIColor, attemptSynchronousLoad: Bool) { var nextIndexById: [Int64: Int] = [:] var validIds: [InlineStickerItemLayer.Key] = [] @@ -165,7 +168,7 @@ public final class TextNodeWithEntities { if let current = self.inlineStickerItemLayers[id] { itemLayer = current } else { - itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: false, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize)) + itemLayer = InlineStickerItemLayer(context: context, groupId: "inlineEmoji", attemptSynchronousLoad: attemptSynchronousLoad, emoji: stickerItem.emoji, file: stickerItem.file, cache: cache, renderer: renderer, placeholderColor: placeholderColor, pointSize: CGSize(width: itemSize, height: itemSize)) self.inlineStickerItemLayers[id] = itemLayer self.textNode.layer.addSublayer(itemLayer) diff --git a/submodules/TelegramUI/Resources/Animations/emojicat_activity.json b/submodules/TelegramUI/Resources/Animations/emojicat_activity.json new file mode 100644 index 0000000000..9c2695c402 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/emojicat_activity.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":60,"w":30,"h":30,"nm":"sport 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Ellipse 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[15]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[15]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[15]},{"t":30,"s":[15]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[23.833]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[17.667]},{"t":30,"s":[15]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[6.667,8.333,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[16.667,13.333,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[16.667,16.667,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[16.667,15,100]},{"t":30,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[23,23],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector 65","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-0.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[4.5,-0.25],[0,-4.25],[-4.5,-0.25],[-2,4.25],[2,4.25]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 65","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Vector 68","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-34.5,46.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[1.252,-1],[3.75,-4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-3.75,-1],[1.252,-1]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3.75,4],[1.252,-1]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 68","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Vector 67","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-48,-19.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.5,1],[3.5,3]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-0.5,-4],[0.5,1]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-3.5,4],[0.5,1]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 67","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Vector 66","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.012,-43.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-0.5],[-0.002,3.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-4,-3.5],[0,-0.5]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4,-3.5],[0,-0.5]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 66","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Vector 68","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,46.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-1.25,-1],[-3.75,-4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3.75,-1],[-1.25,-1]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-3.75,4],[-1.25,-1]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 68","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Vector 67","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[48,-19.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-0.5,1],[-3.5,3]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.5,-4],[-0.5,1]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3.5,4],[-0.5,1]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 67","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/emojicat_animals.json b/submodules/TelegramUI/Resources/Animations/emojicat_animals.json new file mode 100644 index 0000000000..3c7276770d --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/emojicat_animals.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":60,"w":30,"h":30,"nm":"animals 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Ellipse 43","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38.217,-41.067,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[75,75,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[95,95,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-0.482,0.836],[2.152,1.243],[1.381,-2.391]],"o":[[0.751,-0.449],[1.381,-2.391],[-2.152,-1.243],[0,0]],"v":[[1.633,4.536],[3.53,2.595],[2.133,-3.986],[-4.264,-1.905]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 43","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Ellipse 42","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-38.201,-41.067,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[75,75,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[95,95,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.482,0.836],[-2.152,1.243],[-1.381,-2.391]],"o":[[-0.751,-0.449],[-1.381,-2.391],[2.152,-1.243],[0,0]],"v":[[-1.633,4.536],[-3.53,2.595],[-2.133,-3.986],[4.264,-1.905]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 42","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Ellipse 41","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.001,-33,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[4.08,0],[0.492,-3.946]],"o":[[-0.492,-3.946],[-4.08,0],[0,0]],"v":[[7.938,3.5],[0,-3.5],[-7.938,3.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 41","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Ellipse 40","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,51,0],"ix":2,"l":2},"a":{"a":0,"k":[0,-9.3,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[90,90,100]},{"t":20,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[0,0],[-2.177,0],[-0.891,1.835]],"o":[[0.891,1.835],[2.177,0],[0,0]],"v":[[-4.95,-1.55],[0,1.55],[4.95,-1.55]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[-2.177,0],[-0.891,1.835]],"o":[[0.891,1.835],[2.177,0],[0,0]],"v":[[-4.95,-1.55],[0,1.129],[4.95,-1.55]],"c":false}]},{"t":20,"s":[{"i":[[0,0],[-2.177,0],[-0.891,1.835]],"o":[[0.891,1.835],[2.177,0],[0,0]],"v":[[-4.95,-1.55],[0,1.55],[4.95,-1.55]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 40","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Ellipse 39","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[48,19.799,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.137],[3.038,0],[0,0]],"o":[[1.781,0.91],[0,3.038],[0,0],[0,0]],"v":[[0,-5.2],[3,-0.3],[-2.5,5.2],[-3,5.2]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 39","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Ellipse 38","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-48,19.799,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-2.137],[-3.038,0],[0,0]],"o":[[-1.781,0.91],[0,3.038],[0,0],[0,0]],"v":[[0,-5.2],[-3,-0.3],[2.5,5.2],[3,5.2]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 38","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Oval","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[27,1.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[100,30,100]},{"t":25,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3,3.5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Oval","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-27,1.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[100,30,100]},{"t":25,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3,3.5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Vector 64","parent":11,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.94],"y":[1]},"o":{"x":[0.06],"y":[0]},"t":0,"s":[-0.03]},{"i":{"x":[0.94],"y":[1]},"o":{"x":[0.06],"y":[0]},"t":10,"s":[-0.03]},{"t":20,"s":[-0.03]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[36.155]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[24.337]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[33.655]},{"t":30,"s":[31.155]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.507,0],[0,0],[0.38,-0.336],[0.152,-0.691],[0.113,0.516],[0.617,0.544]],"o":[[0,0],[0.507,0],[-0.617,0.544],[-0.112,0.511],[-0.152,-0.691],[-0.38,-0.336]],"v":[[-1.695,-1.693],[1.695,-1.693],[2.051,-0.619],[0.67,1.307],[-0.67,1.307],[-2.051,-0.619]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 64","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Vector","parent":9,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,9.345,0],"to":[0,-0.455,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[0,6.618,0],"to":[0,0,0],"ti":[0,-0.455,0]},{"t":20,"s":[0,9.345,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,0.75],[0,-1.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.552,0],[0,0.552]],"o":[[0,0],[0,0.552],[-0.552,0],[0,0]],"v":[[2,0.74],[2,0.75],[1,1.75],[0,0.75]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.552,0],[0,0.552]],"o":[[0,0],[0,0.552],[0.552,0],[0,0]],"v":[[-2,0.74],[-2,0.75],[-1,1.75],[0,0.75]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":3,"nm":"Ellipse 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[8.333,8.333,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[18.333,18.333,100]},{"t":20,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/emojicat_flags.json b/submodules/TelegramUI/Resources/Animations/emojicat_flags.json new file mode 100644 index 0000000000..670d52cc11 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/emojicat_flags.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":60,"w":30,"h":30,"nm":"flag 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector 76","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[-20]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[5]},{"t":20,"s":[0]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[6]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[6]},{"t":20,"s":[6]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[9.333]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[17.833]},{"t":20,"s":[17]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[8.333,8.333,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[18.333,18.333,100]},{"t":20,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,9],[0,-9]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":0,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 76","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Rectangle 155","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[54,-33,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[4,0],[0,0],[0,0],[-4,0],[-4,0],[0,0],[0,0],[4,0]],"o":[[-4,0],[0,0],[0,0],[4,0],[4,0],[0,0],[0,0],[-4,0]],"v":[[-4,-4.417],[-9,-6.583],[-9,4.417],[-4,6.583],[4,4.417],[9,6.583],[9,-4.417],[4,-6.583]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[{"i":[[4,0],[0,0],[0,0],[-4,0],[-4,0],[0,0],[0,0],[4,0]],"o":[[-4,0],[0,0],[0,0],[4,0],[4,0],[0,0],[0,0],[-4,0]],"v":[[-4,-6.5],[-9,-4.5],[-9,6.5],[-4,4.5],[4,6.5],[9,4.5],[9,-6.5],[4,-4.5]],"c":true}]},{"t":30,"s":[{"i":[[4,0],[0,0],[0,0],[-4,0],[-4,0],[0,0],[0,0],[4,0]],"o":[[-4,0],[0,0],[0,0],[4,0],[4,0],[0,0],[0,0],[-4,0]],"v":[[-4,-4.417],[-9,-6.583],[-9,4.417],[-4,6.583],[4,4.417],[9,6.583],[9,-4.417],[4,-6.583]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 155","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/emojicat_food.json b/submodules/TelegramUI/Resources/Animations/emojicat_food.json new file mode 100644 index 0000000000..27de3b30d3 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/emojicat_food.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":60,"w":30,"h":30,"nm":"food 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Rectangle 149","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2.668,42.324,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.303,0.185],[0,0],[-0.03,0.119],[0.076,0.074],[0.508,0],[0,0],[0.088,-0.086],[-0.026,-0.103],[-0.434,-0.265],[0,0],[-0.162,-0.036],[-0.143,0.032]],"o":[[0,0],[0.434,-0.265],[0.026,-0.103],[-0.088,-0.086],[0,0],[-0.508,0],[-0.076,0.074],[0.03,0.119],[0,0],[0.303,0.185],[0.143,0.032],[0.162,-0.036]],"v":[[0.834,2.326],[7.544,-1.774],[8.225,-2.291],[8.144,-2.578],[7.294,-2.664],[-7.294,-2.664],[-8.144,-2.578],[-8.225,-2.291],[-7.544,-1.774],[-0.834,2.326],[-0.218,2.64],[0.218,2.64]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 149","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Rectangle 151","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2.668,38.34,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":5,"s":[{"i":[[0,0],[0,0],[0,1.105],[-1.105,0],[0,0],[0,-1.105],[1.105,0],[0,0]],"o":[[0,0],[-1.105,0],[0,-1.105],[0,0],[1.105,0],[0,1.105],[0,0],[0,0]],"v":[[-3,2],[-7,2],[-9,0],[-7,-2],[7,-2],[9,0],[7,2],[3,2]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[{"i":[[0,0],[0,0],[0,1.105],[-1.105,0],[0,0],[0,-1.105],[1.105,0],[0,0]],"o":[[0,0],[-1.105,0],[0,-1.105],[0,0],[1.105,0],[0,1.105],[0,0],[0,0]],"v":[[-3,1.667],[-9.5,1.667],[-11.5,0],[-9.5,-1.875],[9.5,-1.875],[11.5,0],[9.5,1.667],[3,1.667]],"c":false}]},{"t":25,"s":[{"i":[[0,0],[0,0],[0,1.105],[-1.105,0],[0,0],[0,-1.105],[1.105,0],[0,0]],"o":[[0,0],[-1.105,0],[0,-1.105],[0,0],[1.105,0],[0,1.105],[0,0],[0,0]],"v":[[-3,2],[-7,2],[-9,0],[-7,-2],[7,-2],[9,0],[7,2],[3,2]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 151","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Rectangle 150","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2.668,62.34,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[110,110,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.214,-0.109],[-0.096,-0.188],[0,-0.56],[0,0],[0.109,-0.214],[0.188,-0.096],[0.56,0],[0,0],[0.214,0.109],[0.096,0.188],[0,0.56],[0,0],[-0.109,0.214],[-0.188,0.096],[-0.56,0],[0,0]],"o":[[0,0],[0.56,0],[0.188,0.096],[0.109,0.214],[0,0],[0,0.56],[-0.096,0.188],[-0.214,0.109],[0,0],[-0.56,0],[-0.188,-0.096],[-0.109,-0.214],[0,0],[0,-0.56],[0.096,-0.188],[0.214,-0.109],[0,0],[0,0]],"v":[[2.5,-2],[7.4,-2],[8.454,-1.891],[8.891,-1.454],[9,-0.4],[9,0.4],[8.891,1.454],[8.454,1.891],[7.4,2],[-7.4,2],[-8.454,1.891],[-8.891,1.454],[-9,0.4],[-9,-0.4],[-8.891,-1.454],[-8.454,-1.891],[-7.4,-2],[-2.5,-2]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":1,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 150","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-5,"op":65,"st":5,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Rectangle 148","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[2.668,8.34,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,1.667]},"t":10,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[110,110,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.517,0],[-0.495,-1.775],[0.978,0],[0,0],[-0.263,0.942]],"o":[[6.517,0],[0.262,0.942],[0,0],[-0.978,0],[0.495,-1.775]],"v":[[0,-3],[8.857,1.323],[7.306,3],[-7.306,3],[-8.857,1.323]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 148","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-4,"op":65,"st":5,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Vector 61","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-9.332,-43.299,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,-0.183]},"t":5,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[90,90,100]},{"t":25,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.245,-0.041],[0.15,-0.159],[0.155,-0.568],[0,0]],"o":[[0,0],[-0.559,-0.186],[-0.216,0.036],[-0.17,0.181],[0,0],[0,0]],"v":[[3,-1.394],[0.105,-2.359],[-0.979,-2.597],[-1.542,-2.295],[-1.945,-1.262],[-3,2.606]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 61","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Rectangle 147","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-42.332,23.34,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.098,0.239],[0.199,0.11],[0.622,0],[0,0],[0.226,-0.125],[0.086,-0.21],[-0.073,-0.618],[0,0],[-0.116,-0.189],[-0.177,-0.082],[-0.503,0],[0,0]],"o":[[0,0],[0.073,-0.618],[-0.086,-0.21],[-0.226,-0.125],[0,0],[-0.622,0],[-0.199,0.11],[-0.098,0.239],[0,0],[0.059,0.5],[0.103,0.166],[0.201,0.094],[0,0],[0,0]],"v":[[5.147,-5.5],[5.29,-6.713],[5.301,-7.88],[4.86,-8.375],[3.701,-8.5],[-3.701,-8.5],[-4.86,-8.375],[-5.301,-7.88],[-5.29,-6.713],[-3.666,7.087],[-3.462,8.025],[-3.033,8.406],[-2.077,8.5],[-0.5,8.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":1,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 147","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":3,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2,"l":2},"a":{"a":0,"k":[-9.332,14.34,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[8.333,8.333,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[18.333,18.333,100]},{"t":20,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/emojicat_objects.json b/submodules/TelegramUI/Resources/Animations/emojicat_objects.json new file mode 100644 index 0000000000..e45d5c6147 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/emojicat_objects.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":60,"w":30,"h":30,"nm":"lamp 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"Vector 72","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[3.5,0],[-3.5,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":0,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 72","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Vector 66","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.012,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[110,110,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-0.5],[-0.002,3.5]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-0.5],[-0.002,3.5]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-0.5],[-0.002,3.5]],"c":false}]},{"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-0.5],[-0.002,3.5]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-4,-3.5],[0,-0.5]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4,-3.5],[0,-0.5]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-4,-3.5],[0,-0.5]],"c":false}]},{"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4,-3.5],[0,-0.5]],"c":false}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4,-3.5],[0,-0.5]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-4,-3.5],[0,-0.5]],"c":false}]},{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4,-3.5],[0,-0.5]],"c":false}]},{"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-4,-3.5],[0,-0.5]],"c":false}]}],"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 66","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Ellipse 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[-5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[5]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[-5]},{"t":30,"s":[0]}],"ix":10},"p":{"a":0,"k":[15,14.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[8.333,8.333,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[17.5,17.5,100]},{"t":20,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.686,0.418],[0,3.257],[4.971,0],[0,-4.971],[-2.591,-1.58],[0,-0.804],[0,0],[-1.105,0],[0,0],[0,1.105],[0,0]],"o":[[2.591,-1.58],[0,-4.971],[-4.971,0],[0,3.257],[0.686,0.418],[0,0],[0,1.105],[0,0],[1.105,0],[0,0],[0,-0.804]],"v":[[4.679,5.19],[9,-2.5],[0,-11.5],[-9,-2.5],[-4.679,5.19],[-3.5,7.12],[-3.5,9.5],[-1.5,11.5],[1.5,11.5],[3.5,9.5],[3.5,7.12]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/emojicat_places.json b/submodules/TelegramUI/Resources/Animations/emojicat_places.json new file mode 100644 index 0000000000..cd8eb3dcac --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/emojicat_places.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":60,"w":30,"h":30,"nm":"city 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector 70","parent":14,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[24,18,0],"ix":2,"l":2},"a":{"a":0,"k":[0,-9,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[90,90,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-1.105,0],[0,1.105],[0,0]],"o":[[0,0],[0,1.105],[1.105,0],[0,0],[0,0]],"v":[[-2,-1.5],[-2,-0.5],[0,1.5],[2,-0.5],[2,-1.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 70","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector 69","parent":14,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-24,18,0],"ix":2,"l":2},"a":{"a":0,"k":[0,-9,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[90,90,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-1.105,0],[0,1.105],[0,0]],"o":[[0,0],[0,1.105],[1.105,0],[0,0],[0,0]],"v":[[-2,-1.5],[-2,-0.5],[0,1.5],[2,-0.5],[2,-1.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 69","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Ellipse 46","parent":14,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-18,0],"ix":2,"l":2},"a":{"a":0,"k":[0,16.5,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[90,90,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[3.038,0],[0,-3.038]],"o":[[0,-3.038],[-3.038,0],[0,0]],"v":[[5.5,2.75],[0,-2.75],[-5.5,2.75]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":2,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 46","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Ellipse 45","parent":14,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[24,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[150,150,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 45","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Ellipse 53","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[54,15,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[105,105,100]},{"t":20,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 53","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Ellipse 51","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[6,-21,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[105,105,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 51","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":70,"st":10,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Ellipse 52","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[54,-3,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":2,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":12,"s":[105,105,100]},{"t":22,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 52","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":62,"st":2,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Ellipse 49","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[6,-39,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":7,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":17,"s":[105,105,100]},{"t":27,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 49","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":7,"op":67,"st":7,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Ellipse 50","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-18,-21,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":1,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":11,"s":[105,105,100]},{"t":21,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 50","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":61,"st":1,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Ellipse 48","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-18,-39,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":9,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[105,105,100]},{"t":29,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 48","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":9,"op":69,"st":9,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Ellipse 47","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-54,15,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":4,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":14,"s":[105,105,100]},{"t":24,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 47","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4,"op":64,"st":4,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Ellipse 44","parent":14,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-24,0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[150,150,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[2,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 44","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Vector 71","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[15,22,0],"ix":2,"l":2},"a":{"a":0,"k":[0,57,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[8.333,8.333,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[17.5,17.5,100]},{"t":20,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.109,0.214],[0.188,0.096],[0.56,0],[0,0],[0.214,0.109],[0.096,0.188],[0,0.56],[0,0],[0.109,0.214],[0.188,0.096],[0.56,0],[0,0],[0.214,-0.109],[0.096,-0.188],[0,-0.56],[0,0],[0.109,-0.214],[0.188,-0.096],[0.56,0],[0,0],[0.214,-0.109],[0.096,-0.188],[0,-0.56],[0,0]],"o":[[0,0],[0,-0.56],[-0.096,-0.188],[-0.214,-0.109],[0,0],[-0.56,0],[-0.188,-0.096],[-0.109,-0.214],[0,0],[0,-0.56],[-0.096,-0.188],[-0.214,-0.109],[0,0],[-0.56,0],[-0.188,0.096],[-0.109,0.214],[0,0],[0,0.56],[-0.096,0.188],[-0.214,0.109],[0,0],[-0.56,0],[-0.188,0.096],[-0.109,0.214],[0,0],[0,0]],"v":[[12,9.5],[12,-1.9],[11.891,-2.954],[11.454,-3.391],[10.4,-3.5],[5.6,-3.5],[4.546,-3.609],[4.109,-4.046],[4,-5.1],[4,-7.9],[3.891,-8.954],[3.454,-9.391],[2.4,-9.5],[-4.4,-9.5],[-5.454,-9.391],[-5.891,-8.954],[-6,-7.9],[-6,-2.1],[-6.109,-1.046],[-6.546,-0.609],[-7.6,-0.5],[-10.4,-0.5],[-11.454,-0.391],[-11.891,0.046],[-12,1.1],[-12,9.5]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 71","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Rectangle 153","parent":13,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,51,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[110,110,100]},{"t":20,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.203,-0.49],[0,-0.932],[-0.152,-0.368],[-0.49,-0.203],[-0.932,0],[0,0],[-0.368,0.152],[-0.203,0.49],[0,0.932],[0.152,0.368],[0.49,0.203],[0.932,0],[0,0],[0.368,-0.152]],"o":[[-0.152,0.368],[0,0.932],[0.203,0.49],[0.368,0.152],[0,0],[0.932,0],[0.49,-0.203],[0.152,-0.368],[0,-0.932],[-0.203,-0.49],[-0.368,-0.152],[0,0],[-0.932,0],[-0.49,0.203]],"v":[[-7.848,-1.765],[-8,0],[-7.848,1.765],[-6.765,2.848],[-5,3],[5,3],[6.765,2.848],[7.848,1.765],[8,0],[7.848,-1.765],[6.765,-2.848],[5,-3],[-5,-3],[-6.765,-2.848]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 153","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/emojicat_smiles.json b/submodules/TelegramUI/Resources/Animations/emojicat_smiles.json new file mode 100644 index 0000000000..58f8f55f0c --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/emojicat_smiles.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":60,"w":30,"h":30,"nm":"smiles 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Oval","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":21,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[-3]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[-22.545]},{"t":20,"s":[-18]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[100,30,100]},{"t":25,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3,3.5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Oval","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":0,"k":-21,"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[-3]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[-22.545]},{"t":20,"s":[-18]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[100,30,100]},{"t":25,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[3,3.5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Smileys & People","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0.94],"y":[1]},"o":{"x":[0.06],"y":[0]},"t":0,"s":[-0.005]},{"i":{"x":[0.94],"y":[1]},"o":{"x":[0.06],"y":[0]},"t":10,"s":[-0.005]},{"t":20,"s":[-0.005]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[35.413]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[25.867]},{"t":20,"s":[30.413]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[100,95,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.366,-0.327],[2.153,0],[1.398,0.334],[0,-0.48],[-4.256,0],[0,4.256]],"o":[[-1.398,0.334],[-2.153,0],[-1.366,-0.327],[0,4.256],[4.256,0],[0,-0.48]],"v":[[5.498,-3.662],[0,-2.862],[-5.498,-3.662],[-7.706,-3.71],[0,3.996],[7.706,-3.71]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-2.045,0],[-1.076,0.282],[0,-0.307],[1.89,0],[0,2.95],[-0.895,-0.234]],"o":[[2.045,0],[0.895,-0.234],[0,2.949],[-1.683,0],[0,-0.307],[1.076,0.282]],"v":[[-0.009,-1.445],[4.762,-2.158],[6.156,-2.215],[-0.009,0.867],[-6.174,-2.215],[-4.781,-2.158]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[{"i":[[-2.045,0],[-1.076,0.282],[0,-0.307],[1.89,0],[0,2.95],[-0.895,-0.234]],"o":[[2.045,0],[0.895,-0.234],[0,2.949],[-1.683,0],[0,-0.307],[1.076,0.282]],"v":[[-0.009,-1.445],[4.762,-2.158],[6.156,-2.215],[-0.009,1.735],[-6.174,-2.215],[-4.781,-2.158]],"c":true}]},{"t":20,"s":[{"i":[[-2.045,0],[-1.076,0.282],[0,-0.307],[1.89,0],[0,2.95],[-0.895,-0.234]],"o":[[2.045,0],[0.895,-0.234],[0,2.949],[-1.683,0],[0,-0.307],[1.076,0.282]],"v":[[-0.009,-1.445],[4.762,-2.158],[6.156,-2.215],[-0.009,0.867],[-6.174,-2.215],[-4.781,-2.158]],"c":true}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Smileys & People","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Ellipse 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[8.333,8.333,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[18.333,18.333,100]},{"t":20,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[23,23],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/Animations/emojicat_symbols.json b/submodules/TelegramUI/Resources/Animations/emojicat_symbols.json new file mode 100644 index 0000000000..565bc14de9 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/emojicat_symbols.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":60,"w":30,"h":30,"nm":"symbols 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Vector 75","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[26.668,50.093,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[110,110,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.781,0],[0,0],[0.407,0.666],[0,0],[0.39,-0.638],[0,0]],"o":[[0,0],[0.781,0],[0,0],[-0.39,-0.638],[0,0],[-0.407,0.666]],"v":[[-3.717,4.041],[3.717,4.041],[4.57,2.52],[0.853,-3.563],[-0.853,-3.563],[-4.57,2.52]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":1,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 75","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Vector 74","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-45.332,50.34,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":5,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[110,110,100]},{"t":25,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4,-4],[-4,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":2,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 74","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Vector 73","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-45.332,50.34,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":5,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[110,110,100]},{"t":25,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4,4],[-4,-4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":2,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector 73","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Ellipse 54","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[26.668,-21.66,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":5,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[110,110,100]},{"t":25,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-2.485,0],[0,2.485],[2.485,0],[0,-2.485]],"o":[[2.485,0],[0,-2.485],[-2.485,0],[0,2.485]],"v":[[0,4.5],[4.5,0],[0,-4.5],[-4.5,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 54","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Rectangle 154","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-45.332,-21.66,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[110,110,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.192,-0.376],[0,-1.12],[0,0],[-0.218,-0.428],[-0.376,-0.192],[-1.12,0],[0,0],[-0.428,0.218],[-0.192,0.376],[0,1.12],[0,0],[0.218,0.428],[0.376,0.192],[1.12,0],[0,0],[0.428,-0.218]],"o":[[-0.218,0.428],[0,0],[0,1.12],[0.192,0.376],[0.428,0.218],[0,0],[1.12,0],[0.376,-0.192],[0.218,-0.428],[0,0],[0,-1.12],[-0.192,-0.376],[-0.428,-0.218],[0,0],[-1.12,0],[-0.376,0.192]],"v":[[-3.782,-2.908],[-4,-0.8],[-4,0.8],[-3.782,2.908],[-2.908,3.782],[-0.8,4],[0.8,4],[2.908,3.782],[3.782,2.908],[4,0.8],[4,-0.8],[3.782,-2.908],[2.908,-3.782],[0.8,-4],[-0.8,-4],[-2.908,-3.782]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 154","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":3,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[-90]},{"t":10,"s":[0]}],"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2,"l":2},"a":{"a":0,"k":[-9.332,14.34,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[8.333,8.333,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[18.333,18.333,100]},{"t":20,"s":[16.667,16.667,100]}],"ix":6,"l":2}},"ao":0,"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/submodules/TelegramUI/Resources/emoji1016.txt b/submodules/TelegramUI/Resources/emoji1016.txt new file mode 100644 index 0000000000..6fef9e8cbe --- /dev/null +++ b/submodules/TelegramUI/Resources/emoji1016.txt @@ -0,0 +1,15 @@ +😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 🥺 😢 😭 😤 😠 😡 🤬 🤯 😳 🥵 🥶 😱 😨 😰 😥 😓 🤗 🤔 🤭 🤫 🤥 😶 😐 😑 😬 🙄 😯 😦 😧 😮 😲 🥱 😴 🤤 😪 😵 🤐 🥴 🤢 🤮 🤧 😷 🤒 🤕 🤑 🤠 😈 👿 👹 👺 🤡 💩 👻 💀 ☠️ 👽 👾 🤖 🎃 😺 😸 😹 😻 😼 😽 🙀 😿 😾 👋 🤚 🖐 ✋ 🖖 👌 🤌 🤏 ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍 👎 ✊ 👊 🤛 🤜 👏 🙌 👐 🤲 🤝 🙏 ✍️ 💅 🤳 💪 🦾 🦵 🦿 🦶 👣 👂 🦻 👃 🫀 🫁 🧠 🦷 🦴 👀 👁 👅 👄 💋 🩸 👶 👧 🧒 👦 👩 🧑 👨 👩‍🦱 🧑‍🦱 👨‍🦱 👩‍🦰 🧑‍🦰 👨‍🦰 👱‍♀️ 👱 👱‍♂️ 👩‍🦳 🧑‍🦳 👨‍🦳 👩‍🦲 🧑‍🦲 👨‍🦲 🧔 👵 🧓 👴 👲 👳‍♀️ 👳 👳‍♂️ 🧕 👮‍♀️ 👮 👮‍♂️ 👷‍♀️ 👷 👷‍♂️ 💂‍♀️ 💂 💂‍♂️ 🕵️‍♀️ 🕵️ 🕵️‍♂️ 👩‍⚕️ 🧑‍⚕️ 👨‍⚕️ 👩‍🌾 🧑‍🌾 👨‍🌾 👩‍🍳 🧑‍🍳 👨‍🍳 👩‍🎓 🧑‍🎓 👨‍🎓 👩‍🎤 🧑‍🎤 👨‍🎤 👩‍🏫 🧑‍🏫 👨‍🏫 👩‍🏭 🧑‍🏭 👨‍🏭 👩‍💻 🧑‍💻 👨‍💻 👩‍💼 🧑‍💼 👨‍💼 👩‍🔧 🧑‍🔧 👨‍🔧 👩‍🔬 🧑‍🔬 👨‍🔬 👩‍🎨 🧑‍🎨 👨‍🎨 👩‍🚒 🧑‍🚒 👨‍🚒 👩‍✈️ 🧑‍✈️ 👨‍✈️ 👩‍🚀 🧑‍🚀 👨‍🚀 👩‍⚖️ 🧑‍⚖️ 👨‍⚖️ 👰‍♀️ 👰 👰‍♂️ 🤵‍♀️ 🤵 🤵‍♂️ 👸 🤴 🥷 🦸‍♀️ 🦸 🦸‍♂️ 🦹‍♀️ 🦹 🦹‍♂️ 🤶 🧑‍🎄 🎅 🧙‍♀️ 🧙 🧙‍♂️ 🧝‍♀️ 🧝 🧝‍♂️ 🧛‍♀️ 🧛 🧛‍♂️ 🧟‍♀️ 🧟 🧟‍♂️ 🧞‍♀️ 🧞 🧞‍♂️ 🧜‍♀️ 🧜 🧜‍♂️ 🧚‍♀️ 🧚 🧚‍♂️ 👼 🤰 🤱 👩‍🍼 🧑‍🍼 👨‍🍼 🙇‍♀️ 🙇 🙇‍♂️ 💁‍♀️ 💁 💁‍♂️ 🙅‍♀️ 🙅 🙅‍♂️ 🙆‍♀️ 🙆 🙆‍♂️ 🙋‍♀️ 🙋 🙋‍♂️ 🧏‍♀️ 🧏 🧏‍♂️ 🤦‍♀️ 🤦 🤦‍♂️ 🤷‍♀️ 🤷 🤷‍♂️ 🙎‍♀️ 🙎 🙎‍♂️ 🙍‍♀️ 🙍 🙍‍♂️ 💇‍♀️ 💇 💇‍♂️ 💆‍♀️ 💆 💆‍♂️ 🧖‍♀️ 🧖 🧖‍♂️ 💅 🤳 💃 🕺 👯‍♀️ 👯 👯‍♂️ 🕴 👩‍🦽 🧑‍🦽 👨‍🦽 👩‍🦼 🧑‍🦼 👨‍🦼 🚶‍♀️ 🚶 🚶‍♂️ 👩‍🦯 🧑‍🦯 👨‍🦯 🧎‍♀️ 🧎 🧎‍♂️ 🏃‍♀️ 🏃 🏃‍♂️ 🧍‍♀️ 🧍 🧍‍♂️ 👭 🧑‍🤝‍🧑 👬 👫 👩‍❤️‍👩 💑 👨‍❤️‍👨 👩‍❤️‍👨 👩‍❤️‍💋‍👩 💏 👨‍❤️‍💋‍👨 👩‍❤️‍💋‍👨 👪 👨‍👩‍👦 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 👨‍👩‍👧‍👧 👨‍👨‍👦 👨‍👨‍👧 👨‍👨‍👧‍👦 👨‍👨‍👦‍👦 👨‍👨‍👧‍👧 👩‍👩‍👦 👩‍👩‍👧 👩‍👩‍👧‍👦 👩‍👩‍👦‍👦 👩‍👩‍👧‍👧 👨‍👦 👨‍👦‍👦 👨‍👧 👨‍👧‍👦 👨‍👧‍👧 👩‍👦 👩‍👦‍👦 👩‍👧 👩‍👧‍👦 👩‍👧‍👧 🗣 👤 👥 🫂 🧳 🌂 ☂️ 🧵 🪡 🪢 🧶 👓 🕶 🥽 🥼 🦺 👔 👕 👖 🧣 🧤 🧥 🧦 👗 👘 🥻 🩴 🩱 🩲 🩳 👙 👚 👛 👜 👝 🎒 👞 👟 🥾 🥿 👠 👡 🩰 👢 👑 👒 🎩 🎓 🧢 ⛑ 🪖 💄 💍 💼 + +🐶 🐱 🐭 🐹 🐰 🦊 🐻 🐼 🐻‍❄️ 🐨 🐯 🦁 🐮 🐷 🐽 🐸 🐵 🙈 🙉 🙊 🐒 🐔 🐧 🐦 🐤 🐣 🐥 🦆 🦅 🦉 🦇 🐺 🐗 🐴 🦄 🐝 🪱 🐛 🦋 🐌 🐞 🐜 🪰 🪲 🪳 🦟 🦗 🕷 🕸 🦂 🐢 🐍 🦎 🦖 🦕 🐙 🦑 🦐 🦞 🦀 🐡 🐠 🐟 🐬 🐳 🐋 🦈 🐊 🐅 🐆 🦓 🦍 🦧 🦣 🐘 🦛 🦏 🐪 🐫 🦒 🦘 🦬 🐃 🐂 🐄 🐎 🐖 🐏 🐑 🦙 🐐 🦌 🐕 🐩 🦮 🐕‍🦺 🐈 🐈‍⬛ 🪶 🐓 🦃 🦤 🦚 🦜 🦢 🦩 🕊 🐇 🦝 🦨 🦡 🦫 🦦 🦥 🐁 🐀 🐿 🦔 🐾 🐉 🐲 🌵 🎄 🌲 🌳 🌴 🪵 🌱 🌿 ☘️ 🍀 🎍 🪴 🎋 🍃 🍂 🍁 🍄 🐚 🪨 🌾 💐 🌷 🌹 🥀 🌺 🌸 🌼 🌻 🌞 🌝 🌛 🌜 🌚 🌕 🌖 🌗 🌘 🌑 🌒 🌓 🌔 🌙 🌎 🌍 🌏 🪐 💫 ⭐️ 🌟 ✨ ⚡️ ☄️ 💥 🔥 🌪 🌈 ☀️ 🌤 ⛅️ 🌥 ☁️ 🌦 🌧 ⛈ 🌩 🌨 ❄️ ☃️ ⛄️ 🌬 💨 💧 💦 ☔️ ☂️ 🌊 🌫 + +🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🫐 🍈 🍒 🍑 🥭 🍍 🥥 🥝 🍅 🍆 🥑 🥦 🥬 🥒 🌶 🫑 🌽 🥕 🫒 🧄 🧅 🥔 🍠 🥐 🥯 🍞 🥖 🥨 🧀 🥚 🍳 🧈 🥞 🧇 🥓 🥩 🍗 🍖 🦴 🌭 🍔 🍟 🍕 🫓 🥪 🥙 🧆 🌮 🌯 🫔 🥗 🥘 🫕 🥫 🍝 🍜 🍲 🍛 🍣 🍱 🥟 🦪 🍤 🍙 🍚 🍘 🍥 🥠 🥮 🍢 🍡 🍧 🍨 🍦 🥧 🧁 🍰 🎂 🍮 🍭 🍬 🍫 🍿 🍩 🍪 🌰 🥜 🍯 🥛 🍼 🫖 ☕️ 🍵 🧃 🥤 🧋 🍶 🍺 🍻 🥂 🍷 🥃 🍸 🍹 🧉 🍾 🧊 🥄 🍴 🍽 🥣 🥡 🥢 🧂 + +⚽️ 🏀 🏈 ⚾️ 🥎 🎾 🏐 🏉 🥏 🎱 🪀 🏓 🏸 🏒 🏑 🥍 🏏 🪃 🥅 ⛳️ 🪁 🏹 🎣 🤿 🥊 🥋 🎽 🛹 🛼 🛷 ⛸ 🥌 🎿 ⛷ 🏂 🪂 🏋️‍♀️ 🏋️ 🏋️‍♂️ 🤼‍♀️ 🤼 🤼‍♂️ 🤸‍♀️ 🤸 🤸‍♂️ ⛹️‍♀️ ⛹️ ⛹️‍♂️ 🤺 🤾‍♀️ 🤾 🤾‍♂️ 🏌️‍♀️ 🏌️ 🏌️‍♂️ 🏇 🧘‍♀️ 🧘 🧘‍♂️ 🏄‍♀️ 🏄 🏄‍♂️ 🏊‍♀️ 🏊 🏊‍♂️ 🤽‍♀️ 🤽 🤽‍♂️ 🚣‍♀️ 🚣 🚣‍♂️ 🧗‍♀️ 🧗 🧗‍♂️ 🚵‍♀️ 🚵 🚵‍♂️ 🚴‍♀️ 🚴 🚴‍♂️ 🏆 🥇 🥈 🥉 🏅 🎖 🏵 🎗 🎫 🎟 🎪 🤹 🤹‍♂️ 🤹‍♀️ 🎭 🩰 🎨 🎬 🎤 🎧 🎼 🎹 🥁 🪘 🎷 🎺 🪗 🎸 🪕 🎻 🎲 ♟ 🎯 🎳 🎮 🎰 🧩 + +🚗 🚕 🚙 🚌 🚎 🏎 🚓 🚑 🚒 🚐 🛻 🚚 🚛 🚜 🦯 🦽 🦼 🛴 🚲 🛵 🏍 🛺 🚨 🚔 🚍 🚘 🚖 🚡 🚠 🚟 🚃 🚋 🚞 🚝 🚄 🚅 🚈 🚂 🚆 🚇 🚊 🚉 ✈️ 🛫 🛬 🛩 💺 🛰 🚀 🛸 🚁 🛶 ⛵️ 🚤 🛥 🛳 ⛴ 🚢 ⚓️ 🪝 ⛽️ 🚧 🚦 🚥 🚏 🗺 🗿 🗽 🗼 🏰 🏯 🏟 🎡 🎢 🎠 ⛲️ ⛱ 🏖 🏝 🏜 🌋 ⛰ 🏔 🗻 🏕 ⛺️ 🛖 🏠 🏡 🏘 🏚 🏗 🏭 🏢 🏬 🏣 🏤 🏥 🏦 🏨 🏪 🏫 🏩 💒 🏛 ⛪️ 🕌 🕍 🛕 🕋 ⛩ 🛤 🛣 🗾 🎑 🏞 🌅 🌄 🌠 🎇 🎆 🌇 🌆 🏙 🌃 🌌 🌉 🌁 + +⌚️ 📱 📲 💻 ⌨️ 🖥 🖨 🖱 🖲 🕹 🗜 💽 💾 💿 📀 📼 📷 📸 📹 🎥 📽 🎞 📞 ☎️ 📟 📠 📺 📻 🎙 🎚 🎛 🧭 ⏱ ⏲ ⏰ 🕰 ⌛️ ⏳ 📡 🔋 🔌 💡 🔦 🕯 🪔 🧯 🛢 💸 💵 💴 💶 💷 🪙 💰 💳 💎 ⚖️ 🪜 🧰 🪛 🔧 🔨 ⚒ 🛠 ⛏ 🪚 🔩 ⚙️ 🪤 🧱 ⛓ 🧲 🔫 💣 🧨 🪓 🔪 🗡 ⚔️ 🛡 🚬 ⚰️ 🪦 ⚱️ 🏺 🔮 📿 🧿 💈 ⚗️ 🔭 🔬 🕳 🩹 🩺 💊 💉 🩸 🧬 🦠 🧫 🧪 🌡 🧹 🪠 🧺 🧻 🚽 🚰 🚿 🛁 🛀 🧼 🪥 🪒 🧽 🪣 🧴 🛎 🔑 🗝 🚪 🪑 🛋 🛏 🛌 🧸 🪆 🖼 🪞 🪟 🛍 🛒 🎁 🎈 🎏 🎀 🪄 🪅 🎊 🎉 🎎 🏮 🎐 🧧 ✉️ 📩 📨 📧 💌 📥 📤 📦 🏷 🪧 📪 📫 📬 📭 📮 📯 📜 📃 📄 📑 🧾 📊 📈 📉 🗒 🗓 📆 📅 🗑 📇 🗃 🗳 🗄 📋 📁 📂 🗂 🗞 📰 📓 📔 📒 📕 📗 📘 📙 📚 📖 🔖 🧷 🔗 📎 🖇 📐 📏 🧮 📌 📍 ✂️ 🖊 🖋 ✒️ 🖌 🖍 📝 ✏️ 🔍 🔎 🔏 🔐 🔒 🔓 + +❤️ 🧡 💛 💚 💙 💜 🖤 🤍 🤎 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🛗 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 ⚧ 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ⏏️ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ ♾ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 🔜 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ 🟥 🟧 🟨 🟩 🟦 🟪 ⬛️ ⬜️ 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 🕟 🕠 🕡 🕢 🕣 🕤 🕥 🕦 🕧 + +🏳️ 🏴 🏁 🚩 🏳️‍🌈 🏳️‍⚧️ 🏴‍☠️ 🇦🇫 🇦🇽 🇦🇱 🇩🇿 🇦🇸 🇦🇩 🇦🇴 🇦🇮 🇦🇶 🇦🇬 🇦🇷 🇦🇲 🇦🇼 🇦🇺 🇦🇹 🇦🇿 🇧🇸 🇧🇭 🇧🇩 🇧🇧 🇧🇾 🇧🇪 🇧🇿 🇧🇯 🇧🇲 🇧🇹 🇧🇴 🇧🇦 🇧🇼 🇧🇷 🇮🇴 🇻🇬 🇧🇳 🇧🇬 🇧🇫 🇧🇮 🇰🇭 🇨🇲 🇨🇦 🇮🇨 🇨🇻 🇧🇶 🇰🇾 🇨🇫 🇹🇩 🇨🇱 🇨🇳 🇨🇽 🇨🇨 🇨🇴 🇰🇲 🇨🇬 🇨🇩 🇨🇰 🇨🇷 🇨🇮 🇭🇷 🇨🇺 🇨🇼 🇨🇾 🇨🇿 🇩🇰 🇩🇯 🇩🇲 🇩🇴 🇪🇨 🇪🇬 🇸🇻 🇬🇶 🇪🇷 🇪🇪 🇪🇹 🇪🇺 🇫🇰 🇫🇴 🇫🇯 🇫🇮 🇫🇷 🇬🇫 🇵🇫 🇹🇫 🇬🇦 🇬🇲 🇬🇪 🇩🇪 🇬🇭 🇬🇮 🇬🇷 🇬🇱 🇬🇩 🇬🇵 🇬🇺 🇬🇹 🇬🇬 🇬🇳 🇬🇼 🇬🇾 🇭🇹 🇭🇳 🇭🇰 🇭🇺 🇮🇸 🇮🇳 🇮🇩 🇮🇷 🇮🇶 🇮🇪 🇮🇲 🇮🇱 🇮🇹 🇯🇲 🇯🇵 🎌 🇯🇪 🇯🇴 🇰🇿 🇰🇪 🇰🇮 🇽🇰 🇰🇼 🇰🇬 🇱🇦 🇱🇻 🇱🇧 🇱🇸 🇱🇷 🇱🇾 🇱🇮 🇱🇹 🇱🇺 🇲🇴 🇲🇰 🇲🇬 🇲🇼 🇲🇾 🇲🇻 🇲🇱 🇲🇹 🇲🇭 🇲🇶 🇲🇷 🇲🇺 🇾🇹 🇲🇽 🇫🇲 🇲🇩 🇲🇨 🇲🇳 🇲🇪 🇲🇸 🇲🇦 🇲🇿 🇲🇲 🇳🇦 🇳🇷 🇳🇵 🇳🇱 🇳🇨 🇳🇿 🇳🇮 🇳🇪 🇳🇬 🇳🇺 🇳🇫 🇰🇵 🇲🇵 🇳🇴 🇴🇲 🇵🇰 🇵🇼 🇵🇸 🇵🇦 🇵🇬 🇵🇾 🇵🇪 🇵🇭 🇵🇳 🇵🇱 🇵🇹 🇵🇷 🇶🇦 🇷🇪 🇷🇴 🇷🇺 🇷🇼 🇼🇸 🇸🇲 🇸🇦 🇸🇳 🇷🇸 🇸🇨 🇸🇱 🇸🇬 🇸🇽 🇸🇰 🇸🇮 🇬🇸 🇸🇧 🇸🇴 🇿🇦 🇰🇷 🇸🇸 🇪🇸 🇱🇰 🇧🇱 🇸🇭 🇰🇳 🇱🇨 🇵🇲 🇻🇨 🇸🇩 🇸🇷 🇸🇿 🇸🇪 🇨🇭 🇸🇾 🇹🇼 🇹🇯 🇹🇿 🇹🇭 🇹🇱 🇹🇬 🇹🇰 🇹🇴 🇹🇹 🇹🇳 🇹🇷 🇹🇲 🇹🇨 🇹🇻 🇻🇮 🇺🇬 🇺🇦 🇦🇪 🇬🇧 🏴󠁧󠁢󠁥󠁮󠁧󠁿 🏴󠁧󠁢󠁳󠁣󠁴󠁿 🏴󠁧󠁢󠁷󠁬󠁳󠁿 🇺🇳 🇺🇸 🇺🇾 🇺🇿 🇻🇺 🇻🇦 🇻🇪 🇻🇳 🇼🇫 🇪🇭 🇾🇪 🇿🇲 🇿🇼 diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index a05f8bdb55..ae95ba6acc 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3403,7 +3403,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G |> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(false))).start(next: { [weak self] responded in if let strongSelf = self { if !responded { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: nil, text: strongSelf.presentationData.strings.Conversation_InteractiveEmojiSyncTip(EnginePeer(peer).compactDisplayTitle).string, undoText: nil), elevatedLayout: false, action: { _ in return false }), in: .current) + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: nil, text: strongSelf.presentationData.strings.Conversation_InteractiveEmojiSyncTip(EnginePeer(peer).compactDisplayTitle).string, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), in: .current) let _ = ApplicationSpecificNotice.incrementInteractiveEmojiSyncTip(accountManager: strongSelf.context.sharedContext.accountManager, timestamp: currentTimestamp).start() } @@ -8002,7 +8002,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { switch result { case .generic: - strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, title: nil, text: added ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: true, action: { _ in return false }), with: nil) + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, title: nil, text: added ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: true, action: { _ in return false }), with: nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let text: String @@ -8011,7 +8011,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } else { text = strongSelf.presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } - strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: true, action: { [weak self] action in + strongSelf.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: stickerFile, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: true, action: { [weak self] action in if let strongSelf = self { if case .info = action { let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers) @@ -12484,7 +12484,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let _ = (self.context.engine.stickers.loadedStickerPack(reference: stickerPackReference, forceActualized: false) |> deliverOnMainQueue).start(next: { [weak self] stickerPack in if let strongSelf = self, case let .result(info, _, _) = stickerPack { - strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: info.title, text: strongSelf.presentationData.strings.Stickers_PremiumPackInfoText, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView), elevatedLayout: false, action: { [weak self] action in + strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: file, title: info.title, text: strongSelf.presentationData.strings.Stickers_PremiumPackInfoText, undoText: strongSelf.presentationData.strings.Stickers_PremiumPackView, customAction: nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self, action == .undo { let _ = strongSelf.controllerInteraction?.openMessage(message, .default) } @@ -16109,7 +16109,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent let sourceRect = self.sourceRect return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in if let sourceNode = sourceNode { - return (sourceNode, sourceRect ?? sourceNode.bounds) + return (sourceNode.view, sourceRect ?? sourceNode.bounds) } else { return nil } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index fbe73c17ca..fae7867aee 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -2022,7 +2022,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - private let emptyInputView = UIView() + private final class EmptyInputView: UIView, UIInputViewAudioFeedback { + var enableInputClicksWhenVisible: Bool { + return true + } + } + + private let emptyInputView = EmptyInputView() private func chatPresentationInterfaceStateInputView(_ state: ChatPresentationInterfaceState) -> UIView? { switch state.inputMode { case .text: diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index fbf1eca0c6..64aa27d3e5 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -15,6 +15,33 @@ import ComponentDisplayAdapters import SettingsUI import TextFormat import PagerComponent +import AppBundle +import PremiumUI +import AudioToolbox +import UndoUI +import ContextUI +import GalleryUI + +private let staticEmojiMapping: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = { + guard let path = getAppBundle().path(forResource: "emoji1016", ofType: "txt") else { + return [] + } + guard let string = try? String(contentsOf: URL(fileURLWithPath: path)) else { + return [] + } + + var result: [(EmojiPagerContentComponent.StaticEmojiSegment, [String])] = [] + + let orderedSegments = EmojiPagerContentComponent.StaticEmojiSegment.allCases + + let segments = string.components(separatedBy: "\n\n") + for i in 0 ..< min(segments.count, orderedSegments.count) { + let list = segments[i].components(separatedBy: " ") + result.append((orderedSegments[i], list)) + } + + return result +}() final class ChatEntityKeyboardInputNode: ChatInputNode { struct InputData: Equatable { @@ -40,29 +67,70 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled + let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + |> distinctUntilChanged + let emojiInputInteraction = EmojiPagerContentComponent.InputInteraction( - performItemAction: { [weak interfaceInteraction] item, _, _, _ in - guard let interfaceInteraction = interfaceInteraction else { - return - } - var text = "." - var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? - loop: for attribute in item.file.attributes { - switch attribute { - case let .Sticker(displayText, packReference, _): - text = displayText - if let packReference = packReference { - emojiAttribute = ChatTextInputTextCustomEmojiAttribute(stickerPack: packReference, fileId: item.file.fileId.id, file: item.file) - break loop - } - default: - break + performItemAction: { [weak interfaceInteraction, weak controllerInteraction] item, _, _, _ in + let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in + guard let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else { + return } - } - - if let emojiAttribute = emojiAttribute { - interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) - } + + if let file = item.file { + var text = "." + var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? + loop: for attribute in file.attributes { + switch attribute { + case let .CustomEmoji(_, displayText, packReference): + text = displayText + emojiAttribute = ChatTextInputTextCustomEmojiAttribute(stickerPack: packReference, fileId: file.fileId.id, file: file) + break loop + default: + break + } + } + + if file.isPremiumEmoji && !hasPremium { + //TODO:localize + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: "Subscribe to Telegram Premium to unlock this emoji.", undoText: "More", customAction: { [weak controllerInteraction] in + guard let controllerInteraction = controllerInteraction else { + return + } + + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumDemoScreen(context: context, subject: .premiumStickers, action: { + let controller = PremiumIntroScreen(context: context, source: .stickers) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + controllerInteraction.navigationController()?.pushViewController(controller) + + /*let controller = PremiumIntroScreen(context: context, source: .stickers) + controllerInteraction.navigationController()?.pushViewController(controller)*/ + }), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + return + } + + if let emojiAttribute = emojiAttribute { + AudioServicesPlaySystemSound(0x450) + interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])) + } + } else if let staticEmoji = item.staticEmoji { + AudioServicesPlaySystemSound(0x450) + interfaceInteraction.insertText(NSAttributedString(string: staticEmoji, attributes: [:])) + } + }) }, deleteBackwards: { [weak interfaceInteraction] in guard let interfaceInteraction = interfaceInteraction else { @@ -72,6 +140,13 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { }, openStickerSettings: { }, + openPremiumSection: { [weak controllerInteraction] in + guard let controllerInteraction = controllerInteraction else { + return + } + let controller = PremiumIntroScreen(context: context, source: .stickers) + controllerInteraction.navigationController()?.pushViewController(controller) + }, pushController: { [weak controllerInteraction] controller in guard let controllerInteraction = controllerInteraction else { return @@ -103,10 +178,20 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { ) let stickerInputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak interfaceInteraction] item, view, rect, layer in - guard let interfaceInteraction = interfaceInteraction else { - return - } - let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, view, rect, layer) + let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in + guard let interfaceInteraction = interfaceInteraction else { + return + } + if let file = item.file { + if file.isPremiumSticker && !hasPremium { + let controller = PremiumIntroScreen(context: context, source: .stickers) + controllerInteraction.navigationController()?.pushViewController(controller) + + return + } + let _ = interfaceInteraction.sendSticker(.standalone(media: file), false, view, rect, layer) + } + }) }, deleteBackwards: { [weak interfaceInteraction] in guard let interfaceInteraction = interfaceInteraction else { @@ -122,6 +207,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { controller.navigationPresentation = .modal controllerInteraction.navigationController()?.pushViewController(controller) }, + openPremiumSection: { + }, pushController: { [weak controllerInteraction] controller in guard let controllerInteraction = controllerInteraction else { return @@ -157,31 +244,52 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return } let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false) + }, + openGifContextMenu: { _, _, _, _, _ in } ) let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { return TempBox.shared.tempFile(fileName: "file").path }) - let animationRenderer = MultiAnimationRendererImpl() + let animationRenderer: MultiAnimationRenderer + /*if #available(iOS 13.0, *) { + animationRenderer = MultiAnimationMetalRendererImpl() + } else {*/ + animationRenderer = MultiAnimationRendererImpl() + //} let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers] let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] - let emojiItems: Signal = context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000) - |> map { view -> EmojiPagerContentComponent in + let emojiItems: Signal = combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), + hasPremium + ) + |> map { view, hasPremium -> EmojiPagerContentComponent in struct ItemGroup { + var supergroupId: AnyHashable var id: AnyHashable + var isPremium: Bool var items: [EmojiPagerContentComponent.Item] } var itemGroups: [ItemGroup] = [] var itemGroupIndexById: [AnyHashable: Int] = [:] - var emojiCollectionIds = Set() - for (id, info, _) in view.collectionInfos { - if let info = info as? StickerPackCollectionInfo { - if info.shortName.lowercased().contains("emoji") { - emojiCollectionIds.insert(id) + for (subgroupId, list) in staticEmojiMapping { + let groupId: AnyHashable = "static" + for emojiString in list { + let resultItem = EmojiPagerContentComponent.Item( + file: nil, + staticEmoji: emojiString, + subgroupId: subgroupId.rawValue + ) + + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, isPremium: false, items: [resultItem])) } } } @@ -190,22 +298,28 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { guard let item = entry.item as? StickerPackItem else { continue } - if item.file.isAnimatedSticker || item.file.isVideoSticker { - if emojiCollectionIds.contains(entry.index.collectionId) { - let resultItem = EmojiPagerContentComponent.Item( - emoji: "", - file: item.file, - stickerPackItem: nil - ) - - let groupId = entry.index.collectionId - if let groupIndex = itemGroupIndexById[groupId] { - itemGroups[groupIndex].items.append(resultItem) - } else { - itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) - } - } + let resultItem = EmojiPagerContentComponent.Item( + file: item.file, + staticEmoji: nil, + subgroupId: nil + ) + + let supergroupId = entry.index.collectionId + let groupId: AnyHashable = supergroupId + let isPremium: Bool = item.file.isPremiumEmoji && !hasPremium + if isPremium && 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])) } } @@ -229,28 +343,21 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } - return EmojiPagerContentComponent.ItemGroup(id: group.id, title: title, items: group.items) + return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: title, isPremium: group.isPremium, displayPremiumBadges: false, items: group.items) }, itemLayoutType: .compact ) } - let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> map { peer -> Bool in - guard case let .user(user) = peer else { - return false - } - return user.isPremium - } - |> distinctUntilChanged - let stickerItems: Signal = combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000), hasPremium ) |> map { view, hasPremium -> EmojiPagerContentComponent in struct ItemGroup { + var supergroupId: AnyHashable var id: AnyHashable + var displayPremiumBadges: Bool var items: [EmojiPagerContentComponent.Item] } var itemGroups: [ItemGroup] = [] @@ -282,9 +389,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } let resultItem = EmojiPagerContentComponent.Item( - emoji: "", file: item.file, - stickerPackItem: nil + staticEmoji: nil, + subgroupId: nil ) let groupId = "saved" @@ -292,7 +399,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: false, items: [resultItem])) } } } @@ -308,9 +415,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } let resultItem = EmojiPagerContentComponent.Item( - emoji: "", file: item.media, - stickerPackItem: nil + staticEmoji: nil, + subgroupId: nil ) let groupId = "recent" @@ -318,7 +425,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: false, items: [resultItem])) } count += 1 @@ -357,9 +464,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { processedIds.insert(item.media.fileId) let resultItem = EmojiPagerContentComponent.Item( - emoji: "", file: item.media, - stickerPackItem: nil + staticEmoji: nil, + subgroupId: nil ) let groupId = "premium" @@ -367,7 +474,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: false, items: [resultItem])) } } } @@ -377,16 +484,16 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { continue } let resultItem = EmojiPagerContentComponent.Item( - emoji: "", file: item.file, - stickerPackItem: item + staticEmoji: nil, + subgroupId: nil ) let groupId = entry.index.collectionId if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count - itemGroups.append(ItemGroup(id: groupId, items: [resultItem])) + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, displayPremiumBadges: true, items: [resultItem])) } } @@ -416,7 +523,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } - return EmojiPagerContentComponent.ItemGroup(id: group.id, title: title, items: group.items) + return EmojiPagerContentComponent.ItemGroup(supergroupId: group.supergroupId, groupId: group.id, title: title, isPremium: false, displayPremiumBadges: group.displayPremiumBadges, items: group.items) }, itemLayoutType: .detailed ) @@ -599,6 +706,12 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return } let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false) + }, + openGifContextMenu: { [weak self] file, sourceView, sourceRect, gesture, isSaved in + guard let strongSelf = self else { + return + } + strongSelf.openGifContextMenu(file: file, sourceView: sourceView, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) } ) @@ -790,4 +903,163 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return (expandedHeight, 0.0) } + + private func openGifContextMenu(file: TelegramMediaFile, sourceView: UIView, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { + let canSaveGif: Bool + if file.fileId.namespace == Namespaces.Media.CloudFile { + canSaveGif = true + } else { + canSaveGif = false + } + + let _ = (self.context.engine.stickers.isGifSaved(id: file.fileId) + |> deliverOnMainQueue).start(next: { [weak self] isGifSaved in + guard let strongSelf = self else { + return + } + var isGifSaved = isGifSaved + if !canSaveGif { + isGifSaved = false + } + let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } + + let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:]) + + let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in + }, baseNavigationController: nil) + gallery.setHintWillBePresentedInPreviewingContext(true) + + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: presentationData.strings.MediaPicker_Send, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + if isSaved { + let _ = self?.controllerInteraction.sendGif(FileMediaReference.savedGif(media: file), sourceView, sourceRect, false, false) + }/* else if let (collection, result) = file.contextResult { + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) + }*/ + }))) + + /*if let (_, _, _, _, _, _, _, _, interfaceState, _, _, _) = strongSelf.validLayout { + var isScheduledMessages = false + if case .scheduledMessages = interfaceState.subject { + isScheduledMessages = true + } + if !isScheduledMessages { + if case let .peer(peerId) = interfaceState.chatLocation { + if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { + items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + if isSaved { + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, true, false) + } else if let (collection, result) = file.contextResult { + let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, true) + } + }))) + } + + if isSaved { + items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.default) + + let _ = self?.controllerInteraction.sendGif(file.file, sourceNode.view, sourceRect, false, true) + }))) + } + } + } + }*/ + + if isSaved || isGifSaved { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } + let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.fileId).start() + }))) + } else if canSaveGif && !isGifSaved { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Preview_SaveGif, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) + }, action: { _, f in + f(.dismissWithoutContent) + + guard let strongSelf = self else { + return + } + + let context = strongSelf.context + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let controllerInteraction = strongSelf.controllerInteraction + let _ = (toggleGifSaved(account: context.account, fileReference: FileMediaReference.savedGif(media: file), saved: true) + |> deliverOnMainQueue).start(next: { result in + switch result { + case .generic: + controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) + case let .limitExceeded(limit, premiumLimit): + let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) + let text: String + if limit == premiumLimit || premiumConfiguration.isPremiumDisabled { + text = presentationData.strings.Premium_MaxSavedGifsFinalText + } else { + text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string + } + controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in + if case .info = action { + let controller = PremiumIntroScreen(context: context, source: .savedGifs) + controllerInteraction.navigationController()?.pushViewController(controller) + return true + } + return false + }), nil) + } + }) + }))) + } + + let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceView: sourceView, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + strongSelf.controllerInteraction.presentGlobalOverlayController(contextController, nil) + }) + } +} + +private final class ContextControllerContentSourceImpl: ContextControllerContentSource { + let controller: ViewController + weak var sourceView: UIView? + let sourceRect: CGRect + + let navigationController: NavigationController? = nil + + let passthroughTouches: Bool = false + + init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect) { + self.controller = controller + self.sourceView = sourceView + self.sourceRect = sourceRect + } + + func transitionInfo() -> ContextControllerTakeControllerInfo? { + let sourceView = self.sourceView + let sourceRect = self.sourceRect + return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceView] in + if let sourceView = sourceView { + return (sourceView, sourceRect) + } else { + return nil + } + }) + } + + func animatedIn() { + if let controller = self.controller as? GalleryController { + controller.viewDidAppear(false) + } + } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index 9d952b1199..2ee0280fba 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -1642,7 +1642,7 @@ final class ChatMediaInputNode: ChatInputNode { |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: false, action: { _ in return false }), nil) + strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) let text: String @@ -1651,7 +1651,7 @@ final class ChatMediaInputNode: ChatInputNode { } else { text = strongSelf.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } - strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: false, action: { [weak self] action in + strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self { if case .info = action { let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers) @@ -1795,7 +1795,7 @@ final class ChatMediaInputNode: ChatInputNode { |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: false, action: { _ in return false }), nil) + strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) let text: String @@ -1804,7 +1804,7 @@ final class ChatMediaInputNode: ChatInputNode { } else { text = strongSelf.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } - strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: false, action: { [weak self] action in + strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self { if case .info = action { let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers) @@ -2725,7 +2725,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent let sourceRect = self.sourceRect return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in if let sourceNode = sourceNode { - return (sourceNode, sourceRect) + return (sourceNode.view, sourceRect) } else { return nil } diff --git a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift index 3cd58a48c5..2a9c0d613c 100644 --- a/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageActionItemNode.swift @@ -278,7 +278,8 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode { context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, - placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground + placeholderColor: item.presentationData.theme.theme.chat.message.freeform.withWallpaper.reactionInactiveBackground, + attemptSynchronous: synchronousLoads )) let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: image != nil ? 2 : floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index cf292275b6..c212d171e9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -975,7 +975,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) var viaBotApply: (TextNodeLayout, () -> TextNode)? - var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? + var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? var needsReplyBackground = false var replyMarkup: ReplyMarkupMessageAttribute? @@ -1145,7 +1145,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { layoutSize.height += 4.0 + reactionButtonsSizeAndApply.0.height } - return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in + return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, synchronousLoads in if let strongSelf = self { strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) strongSelf.updateAccessibilityData(accessibilityData) @@ -1308,7 +1308,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } if let (replyInfoSize, replyInfoApply) = replyInfoApply { - let replyInfoNode = replyInfoApply() + let replyInfoNode = replyInfoApply(synchronousLoads) if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index 20e0337914..7f7efef8c2 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -301,7 +301,15 @@ class ChatPresentationContext { self.animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { return TempBox.shared.tempFile(fileName: "file").path }) - self.animationRenderer = MultiAnimationRendererImpl() + + let animationRenderer: MultiAnimationRenderer + /*if #available(iOS 13.0, *) { + animationRenderer = MultiAnimationMetalRendererImpl() + } else {*/ + animationRenderer = MultiAnimationRendererImpl() + //} + + self.animationRenderer = animationRenderer } } @@ -1035,7 +1043,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode authorNameLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), adminBadgeLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode), - replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, () -> ChatMessageReplyInfoNode), + replyInfoLayout: (ChatMessageReplyInfoNode.Arguments) -> (CGSize, (Bool) -> ChatMessageReplyInfoNode), actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageActionButtonsNode)), reactionButtonsLayout: (ChatMessageReactionButtonsNode.Arguments) -> (minWidth: CGFloat, layout: (CGFloat) -> (size: CGSize, apply: (ListViewItemUpdateAnimation) -> ChatMessageReactionButtonsNode)), mosaicStatusLayout: (ChatMessageDateAndStatusNode.Arguments) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode)), @@ -1691,7 +1699,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode var adminNodeSizeApply: (CGSize, () -> TextNode?) = (CGSize(), { nil }) var replyInfoOriginY: CGFloat = 0.0 - var replyInfoSizeApply: (CGSize, () -> ChatMessageReplyInfoNode?) = (CGSize(), { nil }) + var replyInfoSizeApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode?) = (CGSize(), { _ in nil }) var forwardInfoOriginY: CGFloat = 0.0 var forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode?) = (CGSize(), { _ in nil }) @@ -1809,7 +1817,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode animationCache: item.controllerInteraction.presentationContext.animationCache, animationRenderer: item.controllerInteraction.presentationContext.animationRenderer )) - replyInfoSizeApply = (sizeAndApply.0, { sizeAndApply.1() }) + replyInfoSizeApply = (sizeAndApply.0, { synchronousLoads in sizeAndApply.1(synchronousLoads) }) replyInfoOriginY = headerSize.height headerSize.width = max(headerSize.width, replyInfoSizeApply.0.width + layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right) @@ -2283,7 +2291,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode contentUpperRightCorner: CGPoint, forwardInfoSizeApply: (CGSize, (CGFloat) -> ChatMessageForwardInfoNode?), forwardInfoOriginY: CGFloat, - replyInfoSizeApply: (CGSize, () -> ChatMessageReplyInfoNode?), + replyInfoSizeApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode?), replyInfoOriginY: CGFloat, removedContentNodeIndices: [Int]?, addedContentNodes: [(Message, Bool, ChatMessageBubbleContentNode)]?, @@ -2489,7 +2497,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode } } - if let replyInfoNode = replyInfoSizeApply.1() { + if let replyInfoNode = replyInfoSizeApply.1(synchronousLoads) { strongSelf.replyInfoNode = replyInfoNode var animateFrame = true if replyInfoNode.supernode == nil { diff --git a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift index 1034a9c0a5..b9e933d3f8 100644 --- a/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageInstantVideoItemNode.swift @@ -393,7 +393,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD let videoFrame = CGRect(origin: CGPoint(x: (incoming ? (params.leftInset + layoutConstants.bubble.edgeInset + effectiveAvatarInset + layoutConstants.bubble.contentInsets.left) : (params.width - params.rightInset - videoLayout.contentSize.width - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - deliveryFailedInset)), y: 0.0), size: videoLayout.contentSize) var viaBotApply: (TextNodeLayout, () -> TextNode)? - var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? + var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? var updatedReplyBackgroundNode: NavigationBackgroundNode? var replyMarkup: ReplyMarkupMessageAttribute? @@ -580,7 +580,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD layoutSize.height += 6.0 + reactionButtonsSizeAndApply.0.height } - return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in + return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, synchronousLoads in if let strongSelf = self { strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) @@ -706,7 +706,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD } if let (replyInfoSize, replyInfoApply) = replyInfoApply { - let replyInfoNode = replyInfoApply() + let replyInfoNode = replyInfoApply(synchronousLoads) if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) diff --git a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift index 4e6f278640..ac2d2e9950 100644 --- a/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageReplyInfoNode.swift @@ -90,7 +90,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { self.contentNode.addSubnode(self.lineNode) } - class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, () -> ChatMessageReplyInfoNode) { + class func asyncLayout(_ maybeNode: ChatMessageReplyInfoNode?) -> (_ arguments: Arguments) -> (CGSize, (Bool) -> ChatMessageReplyInfoNode) { let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode) let textNodeLayout = TextNodeWithEntities.asyncLayout(maybeNode?.textNode) let imageNodeLayout = TransformImageNode.asyncLayout(maybeNode?.imageNode) @@ -267,7 +267,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let size = CGSize(width: max(titleLayout.size.width - textInsets.left - textInsets.right, textLayout.size.width - textInsets.left - textInsets.right) + leftInset, height: titleLayout.size.height + textLayout.size.height - 2 * (textInsets.top + textInsets.bottom) + 2 * spacing) - return (size, { + return (size, { attemptSynchronous in let node: ChatMessageReplyInfoNode if let maybeNode = maybeNode { node = maybeNode @@ -283,7 +283,7 @@ class ChatMessageReplyInfoNode: ASDisplayNode { let titleNode = titleApply() var textArguments: TextNodeWithEntities.Arguments? if let cache = arguments.animationCache, let renderer = arguments.animationRenderer { - textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: placeholderColor) + textArguments = TextNodeWithEntities.Arguments(context: arguments.context, cache: cache, renderer: renderer, placeholderColor: placeholderColor, attemptSynchronous: attemptSynchronous) } let textNode = textApply(textArguments) textNode.visibilityRect = node.visibility ? CGRect.infinite : nil diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 941f83c408..1b79c9be75 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -526,7 +526,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let (dateAndStatusSize, dateAndStatusApply) = statusSuggestedWidthAndContinue.1(statusSuggestedWidthAndContinue.0) var viaBotApply: (TextNodeLayout, () -> TextNode)? - var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)? + var replyInfoApply: (CGSize, (Bool) -> ChatMessageReplyInfoNode)? var replyMarkup: ReplyMarkupMessageAttribute? var availableWidth = max(60.0, params.width - params.leftInset - params.rightInset - max(imageSize.width, 160.0) - 20.0 - layoutConstants.bubble.edgeInset * 2.0 - avatarInset - layoutConstants.bubble.contentInsets.left) @@ -762,7 +762,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, _ in + return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _, synchronousLoads in if let strongSelf = self { var transition: ContainedViewLayoutTransition = .immediate if case let .System(duration, _) = animation { @@ -892,7 +892,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } if let (replyInfoSize, replyInfoApply) = replyInfoApply { - let replyInfoNode = replyInfoApply() + let replyInfoNode = replyInfoApply(synchronousLoads) if strongSelf.replyInfoNode == nil { strongSelf.replyInfoNode = replyInfoNode strongSelf.contextSourceNode.contentNode.addSubnode(replyInfoNode) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index d095151145..6d1a5550fe 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -249,6 +249,10 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } } + + if let updatingMedia = item.attributes.updatingMedia { + messageEntities = updatingMedia.entities?.entities ?? [] + } } var entities: [MessageTextEntity]? @@ -404,7 +408,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom - return (boundingSize, { [weak self] animation, _, _ in + return (boundingSize, { [weak self] animation, synchronousLoads, _ in if let strongSelf = self { strongSelf.item = item if let updatedCachedChatMessageText = updatedCachedChatMessageText { @@ -432,7 +436,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } - let _ = textApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: messageTheme.mediaPlaceholderColor)) + let _ = textApply(TextNodeWithEntities.Arguments(context: item.context, cache: item.controllerInteraction.presentationContext.animationCache, renderer: item.controllerInteraction.presentationContext.animationRenderer, placeholderColor: messageTheme.mediaPlaceholderColor, attemptSynchronous: synchronousLoads)) animation.animator.updateFrame(layer: strongSelf.textNode.textNode.layer, frame: textFrame, completion: nil) if let (_, spoilerTextApply) = spoilerTextLayoutAndApply { diff --git a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift index 59f7725103..cfb4bb135a 100644 --- a/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatPinnedMessageTitlePanelNode.swift @@ -685,7 +685,8 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode { context: strongSelf.context, cache: cache, renderer: renderer, - placeholderColor: theme.list.mediaPlaceholderColor + placeholderColor: theme.list.mediaPlaceholderColor, + attemptSynchronous: false ) } let _ = textApply(textArguments) diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift index 936cb29704..2d57f94a97 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsControllerNode.swift @@ -898,7 +898,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { if let navigationController = strongSelf.getNavigationController() { strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .replyThread(message: replyThreadMessage), subject: .message(id: .id(messageId), highlight: true, timecode: nil))) } - case let .stickerPack(name): + case let .stickerPack(name, type): + let _ = type let packReference: StickerPackReference = .name(name) strongSelf.presentController(StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.getNavigationController()), .window(.root), nil) case let .invoice(slug, invoice): diff --git a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift index ec4bbb20e1..e9a78f14e5 100644 --- a/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift +++ b/submodules/TelegramUI/Sources/ChatSearchResultsContollerNode.swift @@ -398,7 +398,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent let sourceNode = self.sourceNode return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in if let sourceNode = sourceNode { - return (sourceNode, sourceNode.bounds) + return (sourceNode.view, sourceNode.bounds) } else { return nil } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 889de57d64..f7de318cab 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -34,6 +34,7 @@ private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.mono private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { private var item: ChatTextInputAccessoryItem private var theme: PresentationTheme + private var strings: PresentationStrings private var width: CGFloat private let iconImageNode: ASImageNode private var animationView: ComponentView? @@ -42,6 +43,7 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { init(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) { self.item = item self.theme = theme + self.strings = strings self.iconImageNode = ASImageNode() @@ -89,6 +91,7 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme + self.strings = strings let (image, text, accessibilityLabel, alpha, insets) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: theme, strings: strings) @@ -156,6 +159,8 @@ private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode { self.item = item if let image = self.iconImageNode.image { + self.iconImageNode.image = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: self.theme, strings: self.strings).0 + let bottomInset: CGFloat = 0.0 let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0) - bottomInset), size: image.size) self.iconImageNode.frame = imageFrame diff --git a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift index cdae308f0c..13b9d4aa81 100644 --- a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift +++ b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift @@ -500,7 +500,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: false, action: { _ in return false }), with: nil) + strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), with: nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) let text: String @@ -509,7 +509,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } else { text = strongSelf.presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } - strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: false, action: { [weak self] action in + strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self { if case .info = action { let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers) @@ -588,7 +588,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: false, action: { _ in return false }), with: nil) + strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.presentationData.strings.Conversation_StickerAddedToFavorites : strongSelf.presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), with: nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) let text: String @@ -597,7 +597,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } else { text = strongSelf.presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } - strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: false, action: { [weak self] action in + strongSelf.controller?.presentInGlobalOverlay(UndoOverlayController(presentationData: strongSelf.presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self { if case .info = action { let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers) diff --git a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift index 8eeca26f7b..f9edf19c44 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift @@ -191,7 +191,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: false, action: { _ in return false }), nil) + strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) let text: String @@ -200,7 +200,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { } else { text = strongSelf.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } - strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: false, action: { [weak self] action in + strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self { if case .info = action { let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers) diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index 4cac521101..c9a9869dd1 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -143,7 +143,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: false, action: { _ in return false }), nil) + strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) let text: String @@ -152,7 +152,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie } else { text = strongSelf.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } - strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: false, action: { [weak self] action in + strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self { if case .info = action { let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index cd1c1377ac..01190bea8b 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -163,7 +163,7 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur present(c, a) }, messageId: replyThreadMessage.messageId, isChannelPost: replyThreadMessage.isChannelPost, atMessage: messageId, displayModalProgress: true).start() } - case let .stickerPack(name): + case let .stickerPack(name, _): dismissInput() /*if false { var mainStickerPack: StickerPackReference? diff --git a/submodules/TelegramUI/Sources/OpenUrl.swift b/submodules/TelegramUI/Sources/OpenUrl.swift index c81e1bfe17..29e075649e 100644 --- a/submodules/TelegramUI/Sources/OpenUrl.swift +++ b/submodules/TelegramUI/Sources/OpenUrl.swift @@ -295,6 +295,22 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur convertedUrl = "https://t.me/addstickers/\(set)" } } + } else if parsedUrl.host == "addemoji" { + if let components = URLComponents(string: "/?" + query) { + var set: String? + if let queryItems = components.queryItems { + for queryItem in queryItems { + if let value = queryItem.value { + if queryItem.name == "set" { + set = value + } + } + } + } + if let set = set { + convertedUrl = "https://t.me/addemoji/\(set)" + } + } } else if parsedUrl.host == "invoice" { if let components = URLComponents(string: "/?" + query) { var slug: String? diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 8dc1c40b17..d1df4d63e6 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -8545,7 +8545,7 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in if let sourceNode = sourceNode { let rect = sourceRect.isEmpty ? sourceNode.bounds : sourceRect - return (sourceNode, rect) + return (sourceNode.view, rect) } else { return nil } diff --git a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift index 3d9e461324..a80f41176a 100644 --- a/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift +++ b/submodules/TelegramUI/Sources/ReplyAccessoryPanelNode.swift @@ -76,7 +76,8 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { context: context, cache: animationCache, renderer: animationRenderer, - placeholderColor: theme.list.mediaPlaceholderColor + placeholderColor: theme.list.mediaPlaceholderColor, + attemptSynchronous: false ) } diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift index 1de8bf6eda..eb919dbc49 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift @@ -147,7 +147,7 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { |> deliverOnMainQueue).start(next: { result in switch result { case .generic: - strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: false, action: { _ in return false }), nil) + strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) let text: String @@ -156,7 +156,7 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { } else { text = strongSelf.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } - strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: false, action: { [weak self] action in + strongSelf.interfaceInteraction?.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self { if case .info = action { let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers) diff --git a/submodules/TelegramUI/Sources/TextLinkHandling.swift b/submodules/TelegramUI/Sources/TextLinkHandling.swift index 7c5934906d..d1098bf776 100644 --- a/submodules/TelegramUI/Sources/TextLinkHandling.swift +++ b/submodules/TelegramUI/Sources/TextLinkHandling.swift @@ -67,7 +67,7 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate controller?.present(c, in: .window(.root), with: a) }, messageId: replyThreadMessage.messageId, isChannelPost: replyThreadMessage.isChannelPost, atMessage: messageId, displayModalProgress: true).start() } - case let .stickerPack(name): + case let .stickerPack(name, _): let packReference: StickerPackReference = .name(name) controller.present(StickerPackScreen(context: context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) case let .instantView(webpage, anchor): @@ -76,12 +76,6 @@ func handleTextLinkActionImpl(context: AccountContext, peerId: PeerId?, navigate controller.present(JoinLinkPreviewController(context: context, link: link, navigateToPeer: { peerId, peekData in openResolvedPeerImpl(peerId, .chat(textInputState: nil, subject: nil, peekData: peekData)) }, parentNavigationController: controller.navigationController as? NavigationController), in: .window(.root)) - #if ENABLE_WALLET - case let .wallet(address, amount, comment): - context.sharedContext.openWallet(context: context, walletContext: .send(address: address, amount: amount, comment: comment)) { c in - (controller.navigationController as? NavigationController)?.pushViewController(c) - } - #endif default: break } diff --git a/submodules/UndoUI/Sources/UndoOverlayController.swift b/submodules/UndoUI/Sources/UndoOverlayController.swift index 4afcdbe5a9..eca1024b52 100644 --- a/submodules/UndoUI/Sources/UndoOverlayController.swift +++ b/submodules/UndoUI/Sources/UndoOverlayController.swift @@ -34,7 +34,7 @@ public enum UndoOverlayContent { case voiceChatRecording(text: String) case voiceChatFlag(text: String) case voiceChatCanSpeak(text: String) - case sticker(context: AccountContext, file: TelegramMediaFile, title: String?, text: String, undoText: String?) + case sticker(context: AccountContext, file: TelegramMediaFile, title: String?, text: String, undoText: String?, customAction: (() -> Void)?) case copy(text: String) case mediaSaved(text: String) case paymentSent(currencyValue: String, itemTitle: String) diff --git a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift index d0bb24e117..6a9a9ab381 100644 --- a/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift +++ b/submodules/UndoUI/Sources/UndoOverlayControllerNode.swift @@ -628,7 +628,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { displayUndo = false self.originalRemainingSeconds = 3 - case let .sticker(context, file, title, text, customUndoText): + case let .sticker(context, file, title, text, customUndoText, _): self.avatarNode = nil self.iconNode = nil self.iconCheckNode = nil @@ -885,7 +885,7 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } else { self.isUserInteractionEnabled = false } - case let .sticker(_, _, _, _, undoText): + case let .sticker(_, _, _, _, undoText, _): self.isUserInteractionEnabled = undoText != nil case .dice: self.panelWrapperNode.clipsToBounds = true @@ -963,7 +963,14 @@ final class UndoOverlayControllerNode: ViewControllerTracingNode { } @objc private func undoButtonPressed() { - let _ = self.action(.undo) + switch self.content { + case let .sticker(_, _, _, _, _, customAction): + if let customAction = customAction { + customAction() + } + default: + let _ = self.action(.undo) + } self.dismiss() } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 1e7392d6b5..6845840629 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -76,7 +76,7 @@ public enum ParsedInternalUrl { case peerName(String, ParsedInternalPeerUrlParameter?) case peerId(PeerId) case privateMessage(messageId: MessageId, threadId: Int32?, timecode: Double?) - case stickerPack(String) + case stickerPack(name: String, type: StickerPackUrlType) case invoice(String) case join(String) case localization(String) @@ -263,7 +263,9 @@ public func parseInternalUrl(query: String) -> ParsedInternalUrl? { return .peerName(peerName, nil) } else if pathComponents.count == 2 || pathComponents.count == 3 { if pathComponents[0] == "addstickers" { - return .stickerPack(pathComponents[1]) + return .stickerPack(name: pathComponents[1], type: .stickers) + } else if pathComponents[0] == "addemoji" { + return .stickerPack(name: pathComponents[1], type: .emoji) } else if pathComponents[0] == "invoice" { return .invoice(pathComponents[1]) } else if pathComponents[0] == "joinchat" || pathComponents[0] == "joinchannel" { @@ -572,8 +574,8 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl) } } } - case let .stickerPack(name): - return .single(.stickerPack(name: name)) + case let .stickerPack(name, type): + return .single(.stickerPack(name: name, type: type)) case let .invoice(slug): return context.engine.payments.fetchBotPaymentInvoice(source: .slug(slug)) |> map(Optional.init) @@ -690,14 +692,14 @@ public func parseStickerPackUrl(_ url: String) -> String? { for scheme in schemes { let basePrefix = scheme + basePath + "/" if url.lowercased().hasPrefix(basePrefix) { - if let internalUrl = parseInternalUrl(query: String(url[basePrefix.endIndex...])), case let .stickerPack(name) = internalUrl { + if let internalUrl = parseInternalUrl(query: String(url[basePrefix.endIndex...])), case let .stickerPack(name, _) = internalUrl { return name } } } } if let parsedUrl = URL(string: url), parsedUrl.scheme == "tg", let host = parsedUrl.host, let query = parsedUrl.query { - if let internalUrl = parseInternalUrl(query: host + "?" + query), case let .stickerPack(name) = internalUrl { + if let internalUrl = parseInternalUrl(query: host + "?" + query), case let .stickerPack(name, _) = internalUrl { return name } } diff --git a/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift b/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift index e127f45cec..63d125ec96 100644 --- a/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift +++ b/submodules/WallpaperBackgroundNode/Sources/MetalWallpaperBackgroundNode.swift @@ -64,6 +64,7 @@ private func makePipelineState(device: MTLDevice, library: MTLLibrary, vertexPro return pipelineState } + @available(iOS 13.0, *) final class MetalWallpaperBackgroundNode: ASDisplayNode, WallpaperBackgroundNode { private let device: MTLDevice diff --git a/submodules/lottie-ios/Sources/Private/CoreAnimation/CoreAnimationLayer.swift b/submodules/lottie-ios/Sources/Private/CoreAnimation/CoreAnimationLayer.swift index 05f140e59f..2507d457d8 100644 --- a/submodules/lottie-ios/Sources/Private/CoreAnimation/CoreAnimationLayer.swift +++ b/submodules/lottie-ios/Sources/Private/CoreAnimation/CoreAnimationLayer.swift @@ -393,6 +393,10 @@ extension CoreAnimationLayer: RootAnimationLayer { func logHierarchyKeypaths() { // Unimplemented / unused } + + func allKeypaths(predicate: (AnimationKeypath) -> Bool) -> [String] { + return [] + } func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) { valueProviderStore.setValueProvider(valueProvider, keypath: keypath) diff --git a/submodules/lottie-ios/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift b/submodules/lottie-ios/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift index d93a7f603c..ffc0d1eab4 100644 --- a/submodules/lottie-ios/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift +++ b/submodules/lottie-ios/Sources/Private/MainThread/LayerContainers/MainThreadAnimationLayer.swift @@ -203,6 +203,14 @@ final class MainThreadAnimationLayer: CALayer, RootAnimationLayer { print("Lottie: Logging Animation Keypaths") animationLayers.forEach({ $0.logKeypaths(for: nil) }) } + + func allKeypaths(predicate: (AnimationKeypath) -> Bool) -> [String] { + var result: [String] = [] + for animationLayer in animationLayers { + result.append(contentsOf: animationLayer.allKeypaths(for: nil, predicate: predicate)) + } + return result + } func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) { for layer in animationLayers { diff --git a/submodules/lottie-ios/Sources/Private/RootAnimationLayer.swift b/submodules/lottie-ios/Sources/Private/RootAnimationLayer.swift index d3ab01085e..ea586f66c4 100644 --- a/submodules/lottie-ios/Sources/Private/RootAnimationLayer.swift +++ b/submodules/lottie-ios/Sources/Private/RootAnimationLayer.swift @@ -32,6 +32,7 @@ protocol RootAnimationLayer: CALayer { func reloadImages() func forceDisplayUpdate() func logHierarchyKeypaths() + func allKeypaths(predicate: (AnimationKeypath) -> Bool) -> [String] func setValueProvider(_ valueProvider: AnyValueProvider, keypath: AnimationKeypath) func getValue(for keypath: AnimationKeypath, atFrame: AnimationFrameTime?) -> Any? diff --git a/submodules/lottie-ios/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift b/submodules/lottie-ios/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift index 08b6e3aa6d..7e31179780 100644 --- a/submodules/lottie-ios/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift +++ b/submodules/lottie-ios/Sources/Private/Utility/Extensions/AnimationKeypathExtension.swift @@ -130,6 +130,29 @@ extension KeypathSearchable { child.logKeypaths(for: newKeypath) } } + + func allKeypaths(for keyPath: AnimationKeypath?, predicate: (AnimationKeypath) -> Bool) -> [String] { + var result: [String] = [] + let newKeypath: AnimationKeypath + if let previousKeypath = keyPath { + newKeypath = previousKeypath.appendingKey(keypathName) + } else { + newKeypath = AnimationKeypath(keys: [keypathName]) + } + if predicate(newKeypath) { + result.append(newKeypath.fullPath) + } + for key in keypathProperties.keys { + let subKey = newKeypath.appendingKey(key) + if predicate(subKey) { + result.append(subKey.fullPath) + } + } + for child in childKeypaths { + result.append(contentsOf: child.allKeypaths(for: newKeypath, predicate: predicate)) + } + return result + } } extension AnimationKeypath { diff --git a/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift b/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift index 55d0c04b68..f2b1b96186 100644 --- a/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift +++ b/submodules/lottie-ios/Sources/Public/Animation/AnimationView.swift @@ -621,6 +621,10 @@ final public class AnimationView: AnimationViewBase { public func logHierarchyKeypaths() { animationLayer?.logHierarchyKeypaths() } + + public func allKeypaths(predicate: (AnimationKeypath) -> Bool) -> [String] { + return animationLayer?.allKeypaths(predicate: predicate) ?? [] + } /// Searches for the nearest child layer to the first Keypath and adds the subview /// to that layer. The subview will move and animate with the child layer. diff --git a/submodules/lottie-ios/Sources/Public/DynamicProperties/AnimationKeypath.swift b/submodules/lottie-ios/Sources/Public/DynamicProperties/AnimationKeypath.swift index f1b6748331..860dfc7da9 100644 --- a/submodules/lottie-ios/Sources/Public/DynamicProperties/AnimationKeypath.swift +++ b/submodules/lottie-ios/Sources/Public/DynamicProperties/AnimationKeypath.swift @@ -44,6 +44,6 @@ public struct AnimationKeypath: Hashable, ExpressibleByStringLiteral { self.keys = keys } - var keys: [String] + public var keys: [String] } From 74b5d4998c77b4b0d01dc44bea30449315c2f4b2 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Wed, 13 Jul 2022 01:44:51 +0200 Subject: [PATCH 2/2] Support static emoji --- .../Sources/EmojiTextAttachmentView.swift | 61 ++++++++++++------- .../Components/LottieAnimationCache/BUILD | 1 + .../Sources/LottieAnimationCache.swift | 13 ++++ 3 files changed, 54 insertions(+), 21 deletions(-) diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index e22da66af9..f0b417afcf 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -163,32 +163,51 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { } let context = self.context - self.disposable = renderer.add(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in - let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) - - let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in - guard let result = result else { - return - } + if file.isAnimatedSticker || file.isVideoEmoji { + self.disposable = renderer.add(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in + let source = AnimatedStickerResourceSource(account: context.account, resource: file.resource, fitzModifier: nil, isVideo: false) - if file.isVideoEmoji { - cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer) - } else { - guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { - writer.finish() + let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in + guard let result = result else { return } - cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) + + if file.isVideoEmoji { + cacheVideoAnimation(path: result, width: Int(size.width), height: Int(size.height), writer: writer) + } else { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: result)) else { + writer.finish() + return + } + cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) + } + }) + + let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: .customEmoji(media: file), resource: file.resource).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() } }) - - let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: .customEmoji(media: file), resource: file.resource).start() - - return ActionDisposable { - dataDisposable.dispose() - fetchDisposable.dispose() - } - }) + } else { + self.disposable = renderer.add(groupId: self.groupId, target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: { size, writer in + let dataDisposable = context.account.postbox.mediaBox.resourceData(file.resource).start(next: { result in + guard result.complete else { + return + } + + cacheStillSticker(path: result.path, width: Int(size.width), height: Int(size.height), writer: writer) + }) + + let fetchDisposable = freeMediaFileResourceInteractiveFetched(account: context.account, fileReference: .customEmoji(media: file), resource: file.resource).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() + } + }) + } } } diff --git a/submodules/TelegramUI/Components/LottieAnimationCache/BUILD b/submodules/TelegramUI/Components/LottieAnimationCache/BUILD index 434120005d..15079a8090 100644 --- a/submodules/TelegramUI/Components/LottieAnimationCache/BUILD +++ b/submodules/TelegramUI/Components/LottieAnimationCache/BUILD @@ -15,6 +15,7 @@ swift_library( "//submodules/Display:Display", "//submodules/rlottie:RLottieBinding", "//submodules/GZip:GZip", + "//submodules/WebPBinding:WebPBinding", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift index 0a738c851f..85ef713466 100644 --- a/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift +++ b/submodules/TelegramUI/Components/LottieAnimationCache/Sources/LottieAnimationCache.swift @@ -4,6 +4,7 @@ import AnimationCache import Display import RLottieBinding import GZip +import WebPBinding public func cacheLottieAnimation(data: Data, width: Int, height: Int, writer: AnimationCacheItemWriter) { writer.queue.async { @@ -29,6 +30,18 @@ public func cacheLottieAnimation(data: Data, width: Int, height: Int, writer: An public func cacheStillSticker(path: String, width: Int, height: Int, writer: AnimationCacheItemWriter) { writer.queue.async { + if let data = try? Data(contentsOf: URL(fileURLWithPath: path)), let image = WebP.convert(fromWebP: data) { + writer.add(with: { surface in + let context = DrawingContext(size: CGSize(width: CGFloat(surface.width), height: CGFloat(surface.height)), scale: 1.0, opaque: false, clear: true, bytesPerRow: surface.bytesPerRow) + context.withContext { c in + UIGraphicsPushContext(c) + c.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: context.size)) + UIGraphicsPopContext() + } + memcpy(surface.argb, context.bytes, surface.height * surface.bytesPerRow) + }, proposedWidth: width, proposedHeight: height, duration: 1.0) + } + writer.finish() } }