diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 723ca92073..6145e30744 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7543,7 +7543,7 @@ Sorry for the inconvenience."; "Channel.AddUserKickedError" = "Sorry, you can't add this user because they are on the list of Removed Users and you can't unban them."; "Channel.AddAdminKickedError" = "Sorry, you can't add this user as an admin because they are in the Removed Users list and you can't unban them."; -"Premium.Stickers.Description" = "Unlock this sticker and more by subscribing to Telegram Premium."; +"Premium.Stickers.Description" = "Unlock this sticker and many more by subscribing to Telegram Premium."; "Premium.Stickers.Proceed" = "Unlock Premium Stickers"; "Premium.Reactions.Proceed" = "Unlock Premium Reactions"; @@ -7592,9 +7592,9 @@ Sorry for the inconvenience."; "Premium.MaxFileSizeNoPremiumText" = "The document can't be sent, because it is larger than **%@**. We are working to let you increase this limit in the future."; "Premium.MaxFileSizeFinalText" = "The document can't be sent, because it is larger than **%@**."; -"Premium.MaxPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some of the currently pinned ones or subscribe to **Telegram Premium** to double the limit to **%2$@** chats."; -"Premium.MaxPinsNoPremiumText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some of the currently pinned ones."; -"Premium.MaxPinsFinalText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some of the currently pinned ones."; +"Premium.MaxPinsText" = "Sorry, you can't pin more than **%1$@** chats to the top. Unpin some that are currently pinned or subscribe to **Telegram Premium** to double the limit to **%2$@** chats."; +"Premium.MaxPinsNoPremiumText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some that are currently pinned."; +"Premium.MaxPinsFinalText" = "Sorry, you can't pin more than **%@** chats to the top. Unpin some that are currently pinned."; "Premium.MaxFavedStickersTitle" = "The Limit of %@ Stickers Reached"; "Premium.MaxFavedStickersText" = "An older sticker was replaced with this one. You can [increase the limit]() to %@ stickers."; @@ -7604,7 +7604,7 @@ Sorry for the inconvenience."; "Premium.MaxSavedGifsText" = "An older GIF was replaced with this one. You can [increase the limit]() to %@ GIFS."; "Premium.MaxSavedGifsFinalText" = "An older GIF was replaced with this one."; -"Premium.MaxAccountsText" = "You have reached the limit of **%@** connected accounts. You can free one place by subscribing to **Telegram Premium**."; +"Premium.MaxAccountsText" = "You have reached the limit of **%@** connected accounts. You can add more by subscribing to **Telegram Premium**."; "Premium.MaxAccountsNoPremiumText" = "You have reached the limit of **%@** connected accounts."; "Premium.MaxAccountsFinalText" = "You have reached the limit of **%@** connected accounts."; @@ -7635,10 +7635,10 @@ Sorry for the inconvenience."; "Premium.NoAds" = "No Ads"; "Premium.NoAdsInfo" = "No more ads in public channels where Telegram sometimes shows ads."; -"Premium.NoAdsStandaloneInfo" = "Remove ads such as this by subscribing to **Telegram Premium**."; +"Premium.NoAdsStandaloneInfo" = "Remove ads such as this one by subscribing to **Telegram Premium**."; "Premium.Reactions" = "Unique Reactions"; -"Premium.ReactionsInfo" = "Additional animated reactions on messages, available only to the Premium subscribers."; +"Premium.ReactionsInfo" = "Additional animated reactions on messages, available only to Premium subscribers."; "Premium.ReactionsStandalone" = "Additional Reactions"; "Premium.ReactionsStandaloneInfo" = "Unlock a wider range of reactions on messages by subscribing to **Telegram Premium**."; @@ -7647,7 +7647,7 @@ Sorry for the inconvenience."; "Premium.StickersInfo" = "Exclusive enlarged stickers featuring additional effects, updated monthly."; "Premium.ChatManagement" = "Advanced Chat Management"; -"Premium.ChatManagementInfo" = "Tools to set default folder, auto-archive and hide new chats."; +"Premium.ChatManagementInfo" = "Tools to set the default folder, auto-archive and hide new chats from non-contacts."; "Premium.Badge" = "Profile Badge"; "Premium.BadgeInfo" = "A badge next to your name showing that you are helping support Telegram."; @@ -7666,10 +7666,10 @@ Sorry for the inconvenience."; "Premium.AboutTitle" = "ABOUT TELEGRAM PREMIUM"; "Premium.AboutText" = "While the free version of Telegram already gives its users more than any other messaging application, **Telegram Premium** pushes its capabilities even further.\n\n**Telegram Premium** is a paid option, because most Premium Features require additional expenses from Telegram to third parties such as data center providers and server manufacturers. Contributions from **Telegram Premium** users allow us to cover such costs and also help Telegram stay free for everyone."; -"Premium.Terms" = "By purchasing a Premium subscription, you agree to our [Terms of Service](terms) and [Privacy Policy](privacy)."; +"Premium.Terms" = "By purchasing a Premium subscription, you agree to the Telegram [Terms of Service](terms) and [Privacy Policy](privacy)."; "Conversation.CopyProtectionSavingDisabledSecret" = "Saving is restricted"; -"Conversation.CopyProtectionForwardingDisabledSecret" = "Forwards are restricted"; +"Conversation.CopyProtectionForwardingDisabledSecret" = "Forwarding is restricted"; "Settings.Terms_URL" = "https://telegram.org/tos"; "Settings.PrivacyPolicy_URL" = "https://telegram.org/privacy"; @@ -7677,7 +7677,7 @@ Sorry for the inconvenience."; "Stickers.PremiumPackInfoText" = "This pack contains premium stickers like this one."; "Stickers.PremiumPackView" = "View"; -"Conversation.PremiumUploadFileTooLarge" = "File could not be sent, because it is larger than 4 GB.\n\nYou can send as many files as you like, but each must be smaller than 4 GB."; +"Conversation.PremiumUploadFileTooLarge" = "File could not be sent because it is larger than 4 GB.\n\nYou can send as many files as you like, but each must be smaller than 4 GB."; "SponsoredMessageMenu.Hide" = "Hide"; @@ -7708,7 +7708,7 @@ Sorry for the inconvenience."; "WebApp.Settings" = "Settings"; -"Bot.AccepRecurrentInfo" = "I accept [Terms of Service]() of **%1$@**"; +"Bot.AccepRecurrentInfo" = "I accept the [Terms of Service]() of **%1$@**"; "Chat.AudioTranscriptionRateAction" = "Rate Transcription"; "Chat.AudioTranscriptionFeedbackTip" = "Thank you for your feedback."; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index a8cc36c4a2..f19aa57c43 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -724,7 +724,7 @@ public protocol SharedAccountContext: AnyObject { func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set) -> Signal func chatAvailableMessageActions(engine: TelegramEngine, accountPeerId: EnginePeer.Id, messageIds: Set, messages: [EngineMessage.Id: EngineMessage], peers: [EnginePeer.Id: EnginePeer]) -> Signal func resolveUrl(context: AccountContext, peerId: PeerId?, url: String, skipUrlAuth: Bool) -> Signal - func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) + func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void) func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) diff --git a/submodules/AccountContext/Sources/OpenChatMessage.swift b/submodules/AccountContext/Sources/OpenChatMessage.swift index 57da66c17d..b5eb81cad0 100644 --- a/submodules/AccountContext/Sources/OpenChatMessage.swift +++ b/submodules/AccountContext/Sources/OpenChatMessage.swift @@ -36,7 +36,7 @@ public final class OpenChatMessageParams { public let openPeer: (Peer, ChatControllerInteractionNavigateToPeer) -> Void public let callPeer: (PeerId, Bool) -> Void public let enqueueMessage: (EnqueueMessage) -> Void - public let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + public let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? public let setupTemporaryHiddenMedia: (Signal, Int, Media) -> Void public let chatAvatarHiddenMedia: (Signal, Media) -> Void public let actionInteraction: GalleryControllerActionInteraction? @@ -63,7 +63,7 @@ public final class OpenChatMessageParams { openPeer: @escaping (Peer, ChatControllerInteractionNavigateToPeer) -> Void, callPeer: @escaping (PeerId, Bool) -> Void, enqueueMessage: @escaping (EnqueueMessage) -> Void, - sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, + sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, setupTemporaryHiddenMedia: @escaping (Signal, Int, Media) -> Void, chatAvatarHiddenMedia: @escaping (Signal, Media) -> Void, actionInteraction: GalleryControllerActionInteraction? = nil, diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm index c73adc2330..7bf26bcf8b 100644 --- a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm +++ b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm @@ -19,14 +19,14 @@ - (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(nullable CGRect *)remainingRect { CGRect result = [super lineFragmentRectForProposedRect:proposedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect]; -#if DEBUG +/*#if DEBUG if (result.origin.y < 10.0f) { result.size.width -= 21.0f; if (result.size.width < 0.0f) { result.size.width = 0.0f; } } -#endif +#endif*/ return result; } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 80203f71c7..011a9cb6af 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -691,6 +691,8 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { }, displayCopyProtectionTip: { _, _ in }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in + }, insertText: { _ in + }, backwardsDeleteText: { }, chatController: { return nil }, statuses: nil) diff --git a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift index 5b2d46fcbb..13e206aff6 100644 --- a/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift +++ b/submodules/ChatPresentationInterfaceState/Sources/ChatPanelInterfaceInteraction.swift @@ -98,7 +98,7 @@ public final class ChatPanelInterfaceInteraction { public let displayVideoUnmuteTip: (CGPoint?) -> Void public let switchMediaRecordingMode: () -> Void public let setupMessageAutoremoveTimeout: () -> Void - public let sendSticker: (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool + public let sendSticker: (FileMediaReference, Bool, UIView, CGRect) -> Bool public let unblockPeer: () -> Void public let pinMessage: (MessageId, ContextControllerProtocol?) -> Void public let unpinMessage: (MessageId, Bool, ContextControllerProtocol?) -> Void @@ -123,7 +123,7 @@ public final class ChatPanelInterfaceInteraction { public let unarchiveChat: () -> Void public let openLinkEditing: () -> Void public let reportPeerIrrelevantGeoLocation: () -> Void - public let displaySlowmodeTooltip: (ASDisplayNode, CGRect) -> Void + public let displaySlowmodeTooltip: (UIView, CGRect) -> Void public let displaySendMessageOptions: (ASDisplayNode, ContextGesture) -> Void public let openScheduledMessages: () -> Void public let displaySearchResultsTooltip: (ASDisplayNode, CGRect) -> Void @@ -144,6 +144,8 @@ public final class ChatPanelInterfaceInteraction { public let displayCopyProtectionTip: (ASDisplayNode, Bool) -> Void public let openWebView: (String, String, Bool, Bool) -> Void public let updateShowWebView: ((Bool) -> Bool) -> Void + public let insertText: (String) -> Void + public let backwardsDeleteText: () -> Void public let chatController: () -> ViewController? public let statuses: ChatPanelInterfaceInteractionStatuses? @@ -192,7 +194,7 @@ public final class ChatPanelInterfaceInteraction { displayVideoUnmuteTip: @escaping (CGPoint?) -> Void, switchMediaRecordingMode: @escaping () -> Void, setupMessageAutoremoveTimeout: @escaping () -> Void, - sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, + sendSticker: @escaping (FileMediaReference, Bool, UIView, CGRect) -> Bool, unblockPeer: @escaping () -> Void, pinMessage: @escaping (MessageId, ContextControllerProtocol?) -> Void, unpinMessage: @escaping (MessageId, Bool, ContextControllerProtocol?) -> Void, @@ -217,7 +219,7 @@ public final class ChatPanelInterfaceInteraction { unarchiveChat: @escaping () -> Void, openLinkEditing: @escaping () -> Void, reportPeerIrrelevantGeoLocation: @escaping () -> Void, - displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, + displaySlowmodeTooltip: @escaping (UIView, CGRect) -> Void, displaySendMessageOptions: @escaping (ASDisplayNode, ContextGesture) -> Void, openScheduledMessages: @escaping () -> Void, openPeersNearby: @escaping () -> Void, @@ -238,6 +240,8 @@ public final class ChatPanelInterfaceInteraction { displayCopyProtectionTip: @escaping (ASDisplayNode, Bool) -> Void, openWebView: @escaping (String, String, Bool, Bool) -> Void, updateShowWebView: @escaping ((Bool) -> Bool) -> Void, + insertText: @escaping (String) -> Void, + backwardsDeleteText: @escaping () -> Void, chatController: @escaping () -> ViewController?, statuses: ChatPanelInterfaceInteractionStatuses? ) { @@ -331,6 +335,9 @@ public final class ChatPanelInterfaceInteraction { self.displayCopyProtectionTip = displayCopyProtectionTip self.openWebView = openWebView self.updateShowWebView = updateShowWebView + self.insertText = insertText + self.backwardsDeleteText = backwardsDeleteText + self.chatController = chatController self.statuses = statuses } @@ -431,6 +438,8 @@ public final class ChatPanelInterfaceInteraction { }, displayCopyProtectionTip: { _, _ in }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in + }, insertText: { _ in + }, backwardsDeleteText: { }, chatController: { return nil }, statuses: nil) diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index dc8a1a4308..1477eb4fbd 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -1,9 +1,17 @@ import Foundation import UIKit +#if targetEnvironment(simulator) +@_silgen_name("UIAnimationDragCoefficient") func UIAnimationDragCoefficient() -> Float +#endif + private extension UIView { static var animationDurationFactor: Double { + #if targetEnvironment(simulator) + return Double(UIAnimationDragCoefficient()) + #else return 1.0 + #endif } } @@ -182,10 +190,12 @@ public struct Transition { switch self.animation { case .none: view.frame = frame + view.layer.removeAnimation(forKey: "position") + view.layer.removeAnimation(forKey: "bounds") completion?(true) case .curve: - let previousPosition = view.center - let previousBounds = view.bounds + let previousPosition = view.layer.presentation()?.position ?? view.center + let previousBounds = view.layer.presentation()?.bounds ?? view.bounds view.frame = frame self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion) @@ -201,9 +211,10 @@ public struct Transition { switch self.animation { case .none: view.bounds = bounds + view.layer.removeAnimation(forKey: "bounds") completion?(true) case .curve: - let previousBounds = view.bounds + let previousBounds = view.layer.presentation()?.bounds ?? view.bounds view.bounds = bounds self.animateBounds(view: view, from: previousBounds, to: view.bounds, completion: completion) @@ -218,9 +229,10 @@ public struct Transition { switch self.animation { case .none: view.center = position + view.layer.removeAnimation(forKey: "position") completion?(true) case .curve: - let previousPosition = view.center + let previousPosition = view.layer.presentation()?.position ?? view.center view.center = position self.animatePosition(view: view, from: previousPosition, to: view.center, completion: completion) @@ -235,9 +247,10 @@ public struct Transition { switch self.animation { case .none: view.alpha = alpha + view.layer.removeAnimation(forKey: "opacity") completion?(true) case .curve: - let previousAlpha = view.alpha + let previousAlpha = (view.layer.presentation()?.opacity).flatMap(CGFloat.init) ?? view.alpha view.alpha = alpha self.animateAlpha(view: view, from: previousAlpha, to: alpha, completion: completion) } diff --git a/submodules/Components/BlurredBackgroundComponent/BUILD b/submodules/Components/BlurredBackgroundComponent/BUILD new file mode 100644 index 0000000000..5474f19bf9 --- /dev/null +++ b/submodules/Components/BlurredBackgroundComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BlurredBackgroundComponent", + module_name = "BlurredBackgroundComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/BlurredBackgroundComponent/Sources/BlurredBackgroundComponent.swift b/submodules/Components/BlurredBackgroundComponent/Sources/BlurredBackgroundComponent.swift new file mode 100644 index 0000000000..0717467711 --- /dev/null +++ b/submodules/Components/BlurredBackgroundComponent/Sources/BlurredBackgroundComponent.swift @@ -0,0 +1,39 @@ +import Foundation +import UIKit +import ComponentFlow +import Display +import ComponentDisplayAdapters + +public final class BlurredBackgroundComponent: Component { + public let color: UIColor + + public init( + color: UIColor + ) { + self.color = color + } + + public static func ==(lhs: BlurredBackgroundComponent, rhs: BlurredBackgroundComponent) -> Bool { + if lhs.color != rhs.color { + return false + } + return true + } + + public final class View: BlurredBackgroundView { + public func update(component: BlurredBackgroundComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.updateColor(color: component.color, transition: transition.containedViewLayoutTransition) + self.update(size: availableSize, transition: transition.containedViewLayoutTransition) + + return availableSize + } + } + + public func makeView() -> View { + return View(color: .clear, enableBlur: true) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift index fb3955c404..e055375462 100644 --- a/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift +++ b/submodules/Components/BundleIconComponent/Sources/BundleIconComponent.swift @@ -7,10 +7,12 @@ import Display public final class BundleIconComponent: Component { public let name: String public let tintColor: UIColor? + public let maxSize: CGSize? - public init(name: String, tintColor: UIColor?) { + public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil) { self.name = name self.tintColor = tintColor + self.maxSize = maxSize } public static func ==(lhs: BundleIconComponent, rhs: BundleIconComponent) -> Bool { @@ -20,6 +22,9 @@ public final class BundleIconComponent: Component { if lhs.tintColor != rhs.tintColor { return false } + if lhs.maxSize != rhs.maxSize { + return false + } return true } @@ -44,7 +49,10 @@ public final class BundleIconComponent: Component { } self.component = component - let imageSize = self.image?.size ?? CGSize() + var imageSize = self.image?.size ?? CGSize() + if let maxSize = component.maxSize { + imageSize = imageSize.aspectFitted(maxSize) + } return CGSize(width: min(imageSize.width, availableSize.width), height: min(imageSize.height, availableSize.height)) } diff --git a/submodules/Components/ComponentDisplayAdapters/BUILD b/submodules/Components/ComponentDisplayAdapters/BUILD new file mode 100644 index 0000000000..50101fc7ad --- /dev/null +++ b/submodules/Components/ComponentDisplayAdapters/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ComponentDisplayAdapters", + module_name = "ComponentDisplayAdapters", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift b/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift new file mode 100644 index 0000000000..34fa9ce017 --- /dev/null +++ b/submodules/Components/ComponentDisplayAdapters/Sources/ComponentDisplayAdapters.swift @@ -0,0 +1,50 @@ +import Foundation +import UIKit +import ComponentFlow +import Display + +public extension Transition.Animation.Curve { + init(_ curve: ContainedViewLayoutTransitionCurve) { + switch curve { + case .linear: + self = .easeInOut + case .easeInOut: + self = .easeInOut + case .custom: + self = .spring + case .customSpring: + self = .spring + case .spring: + self = .spring + } + } + + var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve { + switch self { + case .easeInOut: + return .easeInOut + case .spring: + return .spring + } + } +} + +public extension Transition { + init(_ transition: ContainedViewLayoutTransition) { + switch transition { + case .immediate: + self.init(animation: .none) + case let .animated(duration, curve): + self.init(animation: .curve(duration: duration, curve: Transition.Animation.Curve(curve))) + } + } + + var containedViewLayoutTransition: ContainedViewLayoutTransition { + switch self.animation { + case .none: + return .immediate + case let .curve(duration, curve): + return .animated(duration: duration, curve: curve.containedViewLayoutTransitionCurve) + } + } +} \ No newline at end of file diff --git a/submodules/TelegramUI/Components/EmojiKeyboard/BUILD b/submodules/Components/PagerComponent/BUILD similarity index 72% rename from submodules/TelegramUI/Components/EmojiKeyboard/BUILD rename to submodules/Components/PagerComponent/BUILD index 525cbd2996..ed1b87f808 100644 --- a/submodules/TelegramUI/Components/EmojiKeyboard/BUILD +++ b/submodules/Components/PagerComponent/BUILD @@ -1,8 +1,8 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") swift_library( - name = "EmojiKeyboard", - module_name = "EmojiKeyboard", + name = "PagerComponent", + module_name = "PagerComponent", srcs = glob([ "Sources/**/*.swift", ]), @@ -11,6 +11,7 @@ swift_library( ], deps = [ "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift new file mode 100644 index 0000000000..daa4be7312 --- /dev/null +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -0,0 +1,524 @@ +import Foundation +import UIKit +import Display +import ComponentFlow + +public final class PagerComponentChildEnvironment: Equatable { + public struct ContentScrollingUpdate { + public var relativeOffset: CGFloat + public var absoluteOffsetToClosestEdge: CGFloat? + public var transition: Transition + + public init( + relativeOffset: CGFloat, + absoluteOffsetToClosestEdge: CGFloat?, + transition: Transition + ) { + self.relativeOffset = relativeOffset + self.absoluteOffsetToClosestEdge = absoluteOffsetToClosestEdge + self.transition = transition + } + } + + public let containerInsets: UIEdgeInsets + public let onChildScrollingUpdate: (ContentScrollingUpdate) -> Void + + init( + containerInsets: UIEdgeInsets, + onChildScrollingUpdate: @escaping (ContentScrollingUpdate) -> Void + ) { + self.containerInsets = containerInsets + self.onChildScrollingUpdate = onChildScrollingUpdate + } + + public static func ==(lhs: PagerComponentChildEnvironment, rhs: PagerComponentChildEnvironment) -> Bool { + if lhs.containerInsets != rhs.containerInsets { + return false + } + + return true + } +} + +public final class PagerComponentPanelEnvironment: Equatable { + public let contentOffset: CGFloat + public let contentTopPanels: [AnyComponentWithIdentity] + public let contentIcons: [AnyComponentWithIdentity] + public let contentAccessoryRightButtons: [AnyComponentWithIdentity] + public let activeContentId: AnyHashable? + + init( + contentOffset: CGFloat, + contentTopPanels: [AnyComponentWithIdentity], + contentIcons: [AnyComponentWithIdentity], + contentAccessoryRightButtons: [AnyComponentWithIdentity], + activeContentId: AnyHashable? + ) { + self.contentOffset = contentOffset + self.contentTopPanels = contentTopPanels + self.contentIcons = contentIcons + self.contentAccessoryRightButtons = contentAccessoryRightButtons + self.activeContentId = activeContentId + } + + public static func ==(lhs: PagerComponentPanelEnvironment, rhs: PagerComponentPanelEnvironment) -> Bool { + if lhs.contentOffset != rhs.contentOffset { + return false + } + if lhs.contentTopPanels != rhs.contentTopPanels { + return false + } + if lhs.contentIcons != rhs.contentIcons { + return false + } + if lhs.contentAccessoryRightButtons != rhs.contentAccessoryRightButtons { + return false + } + if lhs.activeContentId != rhs.activeContentId { + return false + } + + return true + } +} + +public struct PagerComponentPanelState { + public var topPanelHeight: CGFloat + + public init(topPanelHeight: CGFloat) { + self.topPanelHeight = topPanelHeight + } +} + +public final class PagerComponent: Component { + public typealias EnvironmentType = ChildEnvironmentType + + public let contentInsets: UIEdgeInsets + public let contents: [AnyComponentWithIdentity<(ChildEnvironmentType, PagerComponentChildEnvironment)>] + public let contentTopPanels: [AnyComponentWithIdentity] + public let contentIcons: [AnyComponentWithIdentity] + public let contentAccessoryRightButtons:[AnyComponentWithIdentity] + public let defaultId: AnyHashable? + public let contentBackground: AnyComponent? + public let topPanel: AnyComponent? + public let externalTopPanelContainer: UIView? + public let bottomPanel: AnyComponent? + public let panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)? + + public init( + contentInsets: UIEdgeInsets, + contents: [AnyComponentWithIdentity<(ChildEnvironmentType, PagerComponentChildEnvironment)>], + contentTopPanels: [AnyComponentWithIdentity], + contentIcons: [AnyComponentWithIdentity], + contentAccessoryRightButtons:[AnyComponentWithIdentity], + defaultId: AnyHashable?, + contentBackground: AnyComponent?, + topPanel: AnyComponent?, + externalTopPanelContainer: UIView?, + bottomPanel: AnyComponent?, + panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)? + ) { + self.contentInsets = contentInsets + self.contents = contents + self.contentTopPanels = contentTopPanels + self.contentIcons = contentIcons + self.contentAccessoryRightButtons = contentAccessoryRightButtons + self.defaultId = defaultId + self.contentBackground = contentBackground + self.topPanel = topPanel + self.externalTopPanelContainer = externalTopPanelContainer + self.bottomPanel = bottomPanel + self.panelStateUpdated = panelStateUpdated + } + + public static func ==(lhs: PagerComponent, rhs: PagerComponent) -> Bool { + if lhs.contentInsets != rhs.contentInsets { + return false + } + if lhs.contents != rhs.contents { + return false + } + if lhs.contentTopPanels != rhs.contentTopPanels { + return false + } + if lhs.contentIcons != rhs.contentIcons { + return false + } + if lhs.defaultId != rhs.defaultId { + return false + } + if lhs.contentBackground != rhs.contentBackground { + return false + } + if lhs.topPanel != rhs.topPanel { + return false + } + if lhs.externalTopPanelContainer !== rhs.externalTopPanelContainer { + return false + } + if lhs.bottomPanel != rhs.bottomPanel { + return false + } + + return true + } + + public final class View: UIView { + private final class ContentView { + let view: ComponentHostView<(ChildEnvironmentType, PagerComponentChildEnvironment)> + var scrollingPanelOffsetToClosestEdge: CGFloat = 0.0 + + init(view: ComponentHostView<(ChildEnvironmentType, PagerComponentChildEnvironment)>) { + self.view = view + } + } + + private struct PaneTransitionGestureState { + var fraction: CGFloat = 0.0 + } + + private var contentViews: [AnyHashable: ContentView] = [:] + private var contentBackgroundView: ComponentHostView? + private var topPanelView: ComponentHostView? + private var bottomPanelView: ComponentHostView? + + private var centralId: AnyHashable? + private var paneTransitionGestureState: PaneTransitionGestureState? + + private var component: PagerComponent? + private weak var state: EmptyComponentState? + + private var panRecognizer: UIPanGestureRecognizer? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.disablesInteractiveTransitionGestureRecognizer = true + + let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) + self.panRecognizer = panRecognizer + self.addGestureRecognizer(panRecognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) { + switch recognizer.state { + case .began: + self.paneTransitionGestureState = PaneTransitionGestureState() + case .changed: + if var paneTransitionGestureState = self.paneTransitionGestureState, self.bounds.width > 0.0 { + paneTransitionGestureState.fraction = recognizer.translation(in: self).x / self.bounds.width + + self.paneTransitionGestureState = paneTransitionGestureState + self.state?.updated(transition: .immediate) + } + case .ended, .cancelled: + if let paneTransitionGestureState = self.paneTransitionGestureState { + self.paneTransitionGestureState = nil + + if paneTransitionGestureState.fraction != 0.0, let component = self.component, let centralId = self.centralId, let centralIndex = component.contents.firstIndex(where: { $0.id == centralId }) { + let fraction = recognizer.translation(in: self).x / self.bounds.width + let velocity = recognizer.velocity(in: self) + + var updatedCentralIndex = centralIndex + if abs(velocity.x) > 180.0 { + if velocity.x > 0.0 { + updatedCentralIndex = max(0, updatedCentralIndex - 1) + } else { + updatedCentralIndex = min(component.contents.count - 1, updatedCentralIndex + 1) + } + } else if abs(fraction) > 0.35 { + if fraction > 0.0 { + updatedCentralIndex = max(0, updatedCentralIndex - 1) + } else { + updatedCentralIndex = min(component.contents.count - 1, updatedCentralIndex + 1) + } + } + if updatedCentralIndex != centralIndex { + self.centralId = component.contents[updatedCentralIndex].id + } + } + + self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring))) + } + default: + break + } + } + + func update(component: PagerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + var centralId: AnyHashable? + if let current = self.centralId { + if component.contents.contains(where: { $0.id == current }) { + centralId = current + } + } + if centralId == nil { + if let defaultId = component.defaultId { + if component.contents.contains(where: { $0.id == defaultId }) { + centralId = defaultId + } + } else { + centralId = component.contents.first?.id + } + } + + if self.centralId != centralId { + self.centralId = centralId + } + + var contentInsets = component.contentInsets + + let scrollingPanelOffsetToClosestEdge: CGFloat + if let centralId = centralId, let centralContentView = self.contentViews[centralId] { + scrollingPanelOffsetToClosestEdge = centralContentView.scrollingPanelOffsetToClosestEdge + } else { + scrollingPanelOffsetToClosestEdge = 0.0 + } + + var topPanelHeight: CGFloat = 0.0 + if let topPanel = component.topPanel { + let topPanelView: ComponentHostView + var topPanelTransition = transition + if let current = self.topPanelView { + topPanelView = current + } else { + topPanelTransition = .immediate + topPanelView = ComponentHostView() + topPanelView.clipsToBounds = true + self.topPanelView = topPanelView + } + + let topPanelSuperview = component.externalTopPanelContainer ?? self + if topPanelView.superview !== topPanelSuperview { + topPanelSuperview.addSubview(topPanelView) + } + + let topPanelSize = topPanelView.update( + transition: topPanelTransition, + component: topPanel, + environment: { + PagerComponentPanelEnvironment( + contentOffset: 0.0, + contentTopPanels: component.contentTopPanels, + contentIcons: [], + contentAccessoryRightButtons: [], + activeContentId: centralId + ) + }, + containerSize: availableSize + ) + + let topPanelOffset = max(0.0, min(topPanelSize.height, scrollingPanelOffsetToClosestEdge)) + + topPanelHeight = max(0.0, topPanelSize.height - topPanelOffset) + + if component.externalTopPanelContainer != nil { + let visibleTopPanelHeight = max(0.0, topPanelSize.height - topPanelOffset) + transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(), size: CGSize(width: topPanelSize.width, height: visibleTopPanelHeight))) + } else { + transition.setFrame(view: topPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelOffset), size: topPanelSize)) + } + + contentInsets.top += topPanelSize.height + } else { + if let bottomPanelView = self.bottomPanelView { + self.bottomPanelView = nil + + bottomPanelView.removeFromSuperview() + } + } + + var bottomPanelOffset: CGFloat = 0.0 + if let bottomPanel = component.bottomPanel { + let bottomPanelView: ComponentHostView + var bottomPanelTransition = transition + if let current = self.bottomPanelView { + bottomPanelView = current + } else { + bottomPanelTransition = .immediate + bottomPanelView = ComponentHostView() + self.bottomPanelView = bottomPanelView + self.addSubview(bottomPanelView) + } + let bottomPanelSize = bottomPanelView.update( + transition: bottomPanelTransition, + component: bottomPanel, + environment: { + PagerComponentPanelEnvironment( + contentOffset: 0.0, + contentTopPanels: [], + contentIcons: component.contentIcons, + contentAccessoryRightButtons: component.contentAccessoryRightButtons, + activeContentId: centralId + ) + }, + containerSize: availableSize + ) + + bottomPanelOffset = max(0.0, min(bottomPanelSize.height, scrollingPanelOffsetToClosestEdge)) + + transition.setFrame(view: bottomPanelView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelSize.height + bottomPanelOffset), size: bottomPanelSize)) + + contentInsets.bottom += bottomPanelSize.height + } else { + if let bottomPanelView = self.bottomPanelView { + self.bottomPanelView = nil + + bottomPanelView.removeFromSuperview() + } + } + + if let contentBackground = component.contentBackground { + let contentBackgroundView: ComponentHostView + var contentBackgroundTransition = transition + if let current = self.contentBackgroundView { + contentBackgroundView = current + } else { + contentBackgroundTransition = .immediate + contentBackgroundView = ComponentHostView() + self.contentBackgroundView = contentBackgroundView + self.insertSubview(contentBackgroundView, at: 0) + } + let contentBackgroundSize = contentBackgroundView.update( + transition: contentBackgroundTransition, + component: contentBackground, + environment: {}, + containerSize: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight - contentInsets.bottom + bottomPanelOffset) + ) + contentBackgroundTransition.setFrame(view: contentBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: contentBackgroundSize)) + } else { + if let contentBackgroundView = self.contentBackgroundView { + self.contentBackgroundView = nil + contentBackgroundView.removeFromSuperview() + } + } + + var validIds: [AnyHashable] = [] + if let centralId = self.centralId, let centralIndex = component.contents.firstIndex(where: { $0.id == centralId }) { + let contentSize = CGSize(width: availableSize.width, height: availableSize.height) + + for index in 0 ..< component.contents.count { + let indexOffset = index - centralIndex + var contentFrame = CGRect(origin: CGPoint(x: contentSize.width * CGFloat(indexOffset), y: 0.0), size: contentSize) + if let paneTransitionGestureState = self.paneTransitionGestureState { + contentFrame.origin.x += paneTransitionGestureState.fraction * availableSize.width + } + let content = component.contents[index] + + let isInBounds = CGRect(origin: CGPoint(), size: availableSize).intersects(contentFrame) + + var isPartOfTransition = false + if case .none = transition.animation { + } else if self.contentViews[content.id] != nil { + isPartOfTransition = true + } + + if isInBounds || isPartOfTransition || content.id == centralId { + let id = content.id + validIds.append(content.id) + + var wasAdded = false + var contentTransition = transition + let contentView: ContentView + if let current = self.contentViews[content.id] { + contentView = current + } else { + wasAdded = true + contentView = ContentView(view: ComponentHostView<(ChildEnvironmentType, PagerComponentChildEnvironment)>()) + contentTransition = .immediate + self.contentViews[content.id] = contentView + if let contentBackgroundView = self.contentBackgroundView { + self.insertSubview(contentView.view, aboveSubview: contentBackgroundView) + } else { + self.insertSubview(contentView.view, at: 0) + } + } + + let pagerChildEnvironment = PagerComponentChildEnvironment( + containerInsets: contentInsets, + onChildScrollingUpdate: { [weak self] update in + guard let strongSelf = self else { + return + } + strongSelf.onChildScrollingUpdate(id: id, update: update) + } + ) + + let _ = contentView.view.update( + transition: contentTransition, + component: content.component, + environment: { + environment[ChildEnvironmentType.self] + pagerChildEnvironment + }, + containerSize: contentFrame.size + ) + + if wasAdded { + contentView.view.frame = contentFrame + } else { + transition.setFrame(view: contentView.view, frame: contentFrame, completion: { [weak self] completed in + if completed && !isInBounds && isPartOfTransition { + DispatchQueue.main.async { + self?.state?.updated(transition: .immediate) + } + } + }) + } + } + } + } + + var removedIds: [AnyHashable] = [] + for (id, contentView) in self.contentViews { + if !validIds.contains(id) { + removedIds.append(id) + contentView.view.removeFromSuperview() + } + } + for id in removedIds { + self.contentViews.removeValue(forKey: id) + } + + if let panelStateUpdated = component.panelStateUpdated { + panelStateUpdated( + PagerComponentPanelState( + topPanelHeight: topPanelHeight + ), + transition + ) + } + + return availableSize + } + + private func onChildScrollingUpdate(id: AnyHashable, update: PagerComponentChildEnvironment.ContentScrollingUpdate) { + guard let contentView = self.contentViews[id] else { + return + } + + if let absoluteOffsetToClosestEdge = update.absoluteOffsetToClosestEdge { + contentView.scrollingPanelOffsetToClosestEdge = absoluteOffsetToClosestEdge + } else { + contentView.scrollingPanelOffsetToClosestEdge = 1000.0 + } + + state?.updated(transition: update.transition) + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public 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) + } +} diff --git a/submodules/Components/ViewControllerComponent/BUILD b/submodules/Components/ViewControllerComponent/BUILD index 0e1e28f483..ca5e4d38f0 100644 --- a/submodules/Components/ViewControllerComponent/BUILD +++ b/submodules/Components/ViewControllerComponent/BUILD @@ -15,6 +15,7 @@ swift_library( "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", "//submodules/AccountContext:AccountContext", "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", ], visibility = [ "//visibility:public", diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index ec7dc58860..9a4a02fa12 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -5,52 +5,7 @@ import Display import SwiftSignalKit import TelegramPresentationData import AccountContext - -public extension Transition.Animation.Curve { - init(_ curve: ContainedViewLayoutTransitionCurve) { - switch curve { - case .linear: - self = .easeInOut - case .easeInOut: - self = .easeInOut - case .custom: - self = .spring - case .customSpring: - self = .spring - case .spring: - self = .spring - } - } - - var containedViewLayoutTransitionCurve: ContainedViewLayoutTransitionCurve { - switch self { - case .easeInOut: - return .easeInOut - case .spring: - return .spring - } - } -} - -public extension Transition { - init(_ transition: ContainedViewLayoutTransition) { - switch transition { - case .immediate: - self.init(animation: .none) - case let .animated(duration, curve): - self.init(animation: .curve(duration: duration, curve: Transition.Animation.Curve(curve))) - } - } - - var containedViewLayoutTransition: ContainedViewLayoutTransition { - switch self.animation { - case .none: - return .immediate - case let .curve(duration, curve): - return .animated(duration: duration, curve: curve.containedViewLayoutTransitionCurve) - } - } -} +import ComponentDisplayAdapters open class ViewControllerComponentContainer: ViewController { public enum NavigationBarAppearance { diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index 3a3c2f00fb..4d01d7ac2c 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -16,6 +16,10 @@ public enum DeviceMetrics: CaseIterable, Equatable { case iPhone12Mini case iPhone12 case iPhone12ProMax + case iPhone13Mini + case iPhone13 + case iPhone13Pro + case iPhone13ProMax case iPad case iPadMini case iPad102Inch @@ -38,6 +42,10 @@ public enum DeviceMetrics: CaseIterable, Equatable { .iPhone12Mini, .iPhone12, .iPhone12ProMax, + .iPhone13Mini, + .iPhone13, + .iPhone13Pro, + .iPhone13ProMax, .iPad, .iPadMini, .iPad102Inch, @@ -113,6 +121,14 @@ public enum DeviceMetrics: CaseIterable, Equatable { return CGSize(width: 390.0, height: 844.0) case .iPhone12ProMax: return CGSize(width: 428.0, height: 926.0) + case .iPhone13Mini: + return CGSize(width: 375.0, height: 812.0) + case .iPhone13: + return CGSize(width: 390.0, height: 844.0) + case .iPhone13Pro: + return CGSize(width: 390.0, height: 844.0) + case .iPhone13ProMax: + return CGSize(width: 428.0, height: 926.0) case .iPad: return CGSize(width: 768.0, height: 1024.0) case .iPadMini: @@ -140,9 +156,9 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 41.0 + UIScreenPixel case .iPhone12Mini: return 44.0 - case .iPhone12: + case .iPhone12, .iPhone13, .iPhone13Pro: return 47.0 + UIScreenPixel - case .iPhone12ProMax: + case .iPhone12ProMax, .iPhone13ProMax: return 53.0 + UIScreenPixel case let .unknown(_, _, onScreenNavigationHeight): if let _ = onScreenNavigationHeight { @@ -157,7 +173,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func safeInsets(inLandscape: Bool) -> UIEdgeInsets { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0) default: return UIEdgeInsets.zero @@ -166,7 +182,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func onScreenNavigationHeight(inLandscape: Bool, systemOnScreenNavigationHeight: CGFloat?) -> CGFloat? { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return inLandscape ? 21.0 : 34.0 case .iPadPro3rdGen, .iPadPro11Inch: return 21.0 @@ -198,7 +214,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { var statusBarHeight: CGFloat { switch self { - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 44.0 case .iPadPro11Inch, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: return 24.0 @@ -216,7 +232,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 162.0 case .iPhone6, .iPhone6Plus: return 163.0 - case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax: + case .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 172.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 348.0 @@ -235,9 +251,9 @@ public enum DeviceMetrics: CaseIterable, Equatable { return 216.0 case .iPhone6Plus: return 226.0 - case .iPhoneX, .iPhone12Mini, .iPhone12: - return 291.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro: + return 292.0 + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax: return 302.0 case .iPad, .iPad102Inch, .iPadPro10Inch: return 263.0 @@ -256,7 +272,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { func predictiveInputHeight(inLandscape: Bool) -> CGFloat { if inLandscape { switch self { - case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax: + case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 37.0 case .iPad, .iPad102Inch, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen, .iPadMini, .iPadMini6thGen: return 50.0 @@ -267,7 +283,7 @@ public enum DeviceMetrics: CaseIterable, Equatable { switch self { case .iPhone4, .iPhone5: return 37.0 - case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax: + case .iPhone6, .iPhoneX, .iPhoneXSMax, .iPhoneXr, .iPhone12Mini, .iPhone12, .iPhone12ProMax, .iPhone13Mini, .iPhone13, .iPhone13Pro, .iPhone13ProMax: return 44.0 case .iPhone6Plus: return 45.0 diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 99bfdc5ac8..1eae1fcc89 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -12,7 +12,7 @@ open class SparseNode: ASDisplayNode { if !self.bounds.contains(point) { return nil } - for view in self.view.subviews { + for view in self.view.subviews.reversed() { if let result = view.hitTest(self.view.convert(point, to: view), with: event), result.isUserInteractionEnabled { return result } @@ -27,6 +27,26 @@ open class SparseNode: ASDisplayNode { } } +open class SparseContainerView: UIView { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.alpha.isZero { + return nil + } + for view in self.subviews.reversed() { + if let result = view.hitTest(self.convert(point, to: view), with: event), result.isUserInteractionEnabled { + return result + } + } + + let result = super.hitTest(point, with: event) + if result != self { + return result + } else { + return nil + } + } +} + public final class NavigationBarTheme { public static func generateBackArrowImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 13.0, height: 22.0), rotatedContext: { size, context in @@ -266,6 +286,146 @@ public final class NavigationBackgroundNode: ASDisplayNode { } } +open class BlurredBackgroundView: UIView { + private var _color: UIColor + + private var enableBlur: Bool + + private var effectView: UIVisualEffectView? + private let backgroundView: UIView + + private var validLayout: (CGSize, CGFloat)? + + public var backgroundCornerRadius: CGFloat { + if let (_, cornerRadius) = self.validLayout { + return cornerRadius + } else { + return 0.0 + } + } + + public init(color: UIColor, enableBlur: Bool = true) { + self._color = .clear + self.enableBlur = enableBlur + + self.backgroundView = UIView() + + super.init(frame: CGRect()) + + self.addSubview(self.backgroundView) + + self.updateColor(color: color, transition: .immediate) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateBackgroundBlur(forceKeepBlur: Bool) { + if self.enableBlur && !sharedIsReduceTransparencyEnabled && ((self._color.alpha > .ulpOfOne && self._color.alpha < 0.95) || forceKeepBlur) { + if self.effectView == nil { + let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light)) + + for subview in effectView.subviews { + if subview.description.contains("VisualEffectSubview") { + subview.isHidden = true + } + } + + if let sublayer = effectView.layer.sublayers?[0], let filters = sublayer.filters { + sublayer.backgroundColor = nil + sublayer.isOpaque = false + let allowedKeys: [String] = [ + "colorSaturate", + "gaussianBlur" + ] + sublayer.filters = filters.filter { filter in + guard let filter = filter as? NSObject else { + return true + } + let filterName = String(describing: filter) + if !allowedKeys.contains(filterName) { + return false + } + return true + } + } + + if let (size, cornerRadius) = self.validLayout { + effectView.frame = CGRect(origin: CGPoint(), size: size) + ContainedViewLayoutTransition.immediate.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius) + effectView.clipsToBounds = !cornerRadius.isZero + } + self.effectView = effectView + self.insertSubview(effectView, at: 0) + } + } else if let effectView = self.effectView { + self.effectView = nil + effectView.removeFromSuperview() + } + } + + public func updateColor(color: UIColor, enableBlur: Bool? = nil, forceKeepBlur: Bool = false, transition: ContainedViewLayoutTransition) { + let effectiveEnableBlur = enableBlur ?? self.enableBlur + + if self._color.isEqual(color) && self.enableBlur == effectiveEnableBlur { + return + } + self._color = color + self.enableBlur = effectiveEnableBlur + + if sharedIsReduceTransparencyEnabled { + transition.updateBackgroundColor(layer: self.backgroundView.layer, color: self._color.withAlphaComponent(1.0)) + } else { + transition.updateBackgroundColor(layer: self.backgroundView.layer, color: self._color) + } + + self.updateBackgroundBlur(forceKeepBlur: forceKeepBlur) + } + + public func update(size: CGSize, cornerRadius: CGFloat = 0.0, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, cornerRadius) + + let contentFrame = CGRect(origin: CGPoint(), size: size) + transition.updateFrame(view: self.backgroundView, frame: contentFrame, beginWithCurrentState: true) + if let effectView = self.effectView, effectView.frame != contentFrame { + transition.updateFrame(layer: effectView.layer, frame: contentFrame, beginWithCurrentState: true) + if let sublayers = effectView.layer.sublayers { + for sublayer in sublayers { + transition.updateFrame(layer: sublayer, frame: contentFrame, beginWithCurrentState: true) + } + } + } + + transition.updateCornerRadius(layer: self.backgroundView.layer, cornerRadius: cornerRadius) + if let effectView = self.effectView { + transition.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius) + effectView.clipsToBounds = !cornerRadius.isZero + } + } + + public func update(size: CGSize, cornerRadius: CGFloat = 0.0, animator: ControlledTransitionAnimator) { + self.validLayout = (size, cornerRadius) + + let contentFrame = CGRect(origin: CGPoint(), size: size) + animator.updateFrame(layer: self.backgroundView.layer, frame: contentFrame, completion: nil) + if let effectView = self.effectView, effectView.frame != contentFrame { + animator.updateFrame(layer: effectView.layer, frame: contentFrame, completion: nil) + if let sublayers = effectView.layer.sublayers { + for sublayer in sublayers { + animator.updateFrame(layer: sublayer, frame: contentFrame, completion: nil) + } + } + } + + animator.updateCornerRadius(layer: self.backgroundView.layer, cornerRadius: cornerRadius, completion: nil) + if let effectView = self.effectView { + animator.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius, completion: nil) + effectView.clipsToBounds = !cornerRadius.isZero + } + } +} + open class NavigationBar: ASDisplayNode { public static var defaultSecondaryContentHeight: CGFloat { return 38.0 diff --git a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift index bb935c0295..14159d8e10 100644 --- a/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift +++ b/submodules/JoinLinkPreviewUI/Sources/JoinLinkPreviewControllerNode.swift @@ -178,7 +178,7 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, UIScrollVi let animation = contentNode.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.25) animation.fillMode = .both if !fastOut { - animation.beginTime = CACurrentMediaTime() + 0.1 + animation.beginTime = contentNode.layer.convertTime(CACurrentMediaTime(), from: nil) + 0.1 } contentNode.layer.add(animation, forKey: "opacity") diff --git a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift index 4e2d372bb9..eabbb8e37a 100644 --- a/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift +++ b/submodules/LanguageLinkPreviewUI/Sources/LanguageLinkPreviewControllerNode.swift @@ -199,7 +199,7 @@ final class LanguageLinkPreviewControllerNode: ViewControllerTracingNode, UIScro let animation = contentNode.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.35) animation.fillMode = .both if !fastOut { - animation.beginTime = CACurrentMediaTime() + 0.1 + animation.beginTime = contentNode.layer.convertTime(CACurrentMediaTime(), from: nil) + 0.1 } contentNode.layer.add(animation, forKey: "opacity") diff --git a/submodules/PasscodeUI/Sources/PasscodeLayout.swift b/submodules/PasscodeUI/Sources/PasscodeLayout.swift index 7fb5e8f78b..bd76dad5b8 100644 --- a/submodules/PasscodeUI/Sources/PasscodeLayout.swift +++ b/submodules/PasscodeUI/Sources/PasscodeLayout.swift @@ -67,7 +67,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 226.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneX, .iPhone12Mini, .iPhone12: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro: self.buttonSize = 75.0 self.horizontalSecond = 103.0 self.horizontalThird = 206.0 @@ -78,7 +78,7 @@ struct PasscodeKeyboardLayout { self.topOffset = 294.0 self.biometricsOffset = 30.0 self.deleteOffset = 20.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax: self.buttonSize = 85.0 self.horizontalSecond = 115.0 self.horizontalThird = 230.0 @@ -151,11 +151,11 @@ public struct PasscodeLayout { self.titleOffset = 112.0 self.subtitleOffset = -6.0 self.inputFieldOffset = 156.0 - case .iPhoneX, .iPhone12Mini, .iPhone12: + case .iPhoneX, .iPhone12Mini, .iPhone12, .iPhone13Mini, .iPhone13, .iPhone13Pro: self.titleOffset = 162.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 206.0 - case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax: + case .iPhoneXSMax, .iPhoneXr, .iPhone12ProMax, .iPhone13ProMax: self.titleOffset = 180.0 self.subtitleOffset = 0.0 self.inputFieldOffset = 226.0 diff --git a/submodules/Postbox/Sources/MediaBox.swift b/submodules/Postbox/Sources/MediaBox.swift index 341b196a8b..5a6a01015f 100644 --- a/submodules/Postbox/Sources/MediaBox.swift +++ b/submodules/Postbox/Sources/MediaBox.swift @@ -188,7 +188,8 @@ public final class MediaBox { self.timeBasedCleanup = TimeBasedCleanup(generalPaths: [ self.basePath, - self.basePath + "/cache" + self.basePath + "/cache", + self.basePath + "/animation-cache" ], shortLivedPaths: [ self.basePath + "/short-cache" ]) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift index 3c598121cf..482a6dc37c 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewController.swift @@ -48,12 +48,12 @@ public final class StickerPackPreviewController: ViewController, StandalonePrese private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - public var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? { + public var sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? { didSet { if self.isNodeLoaded { if let sendSticker = self.sendSticker { - self.controllerNode.sendSticker = { [weak self] file, sourceNode, sourceRect in - if sendSticker(file, sourceNode, sourceRect) { + self.controllerNode.sendSticker = { [weak self] file, sourceView, sourceRect in + if sendSticker(file, sourceView, sourceRect) { self?.dismiss() return true } else { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift index c5bcd464cb..a317390966 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewControllerNode.swift @@ -73,7 +73,7 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol var presentInGlobalOverlay: ((ViewController, Any?) -> Void)? var dismiss: (() -> Void)? var cancel: (() -> Void)? - var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + var sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let actionPerformed: ((StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) -> Void)? let ready = Promise() @@ -220,9 +220,9 @@ final class StickerPackPreviewControllerNode: ViewControllerTracingNode, UIScrol menuItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode, animationNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode, imageNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode.view, imageNode.bounds) } } f(.default) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift index 04b7b114c5..7cb8c4b2b2 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackScreen.swift @@ -85,7 +85,7 @@ private final class StickerPackContainer: ASDisplayNode { private let decideNextAction: (StickerPackContainer, StickerPackAction) -> StickerPackNextAction private let requestDismiss: () -> Void private let presentInGlobalOverlay: (ViewController, Any?) -> Void - private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let backgroundNode: ASImageNode private let gridNode: GridNode private let actionAreaBackgroundNode: NavigationBackgroundNode @@ -129,7 +129,7 @@ private final class StickerPackContainer: ASDisplayNode { private weak var peekController: PeekController? - init(index: Int, context: AccountContext, presentationData: PresentationData, stickerPack: StickerPackReference, decideNextAction: @escaping (StickerPackContainer, StickerPackAction) -> StickerPackNextAction, requestDismiss: @escaping () -> Void, expandProgressUpdated: @escaping (StickerPackContainer, ContainedViewLayoutTransition, ContainedViewLayoutTransition) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, openMention: @escaping (String) -> Void, controller: StickerPackScreenImpl?) { + init(index: Int, context: AccountContext, presentationData: PresentationData, stickerPack: StickerPackReference, decideNextAction: @escaping (StickerPackContainer, StickerPackAction) -> StickerPackNextAction, requestDismiss: @escaping () -> Void, expandProgressUpdated: @escaping (StickerPackContainer, ContainedViewLayoutTransition, ContainedViewLayoutTransition) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, openMention: @escaping (String) -> Void, controller: StickerPackScreenImpl?) { self.index = index self.context = context self.controller = controller @@ -367,9 +367,9 @@ private final class StickerPackContainer: ASDisplayNode { menuItems.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode, animationNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode, imageNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode.view, imageNode.bounds) } } f(.default) @@ -964,7 +964,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { private let modalProgressUpdated: (CGFloat, ContainedViewLayoutTransition) -> Void private let dismissed: () -> Void private let presentInGlobalOverlay: (ViewController, Any?) -> Void - private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let openMention: (String) -> Void private let dimNode: ASDisplayNode @@ -986,7 +986,7 @@ private final class StickerPackScreenNode: ViewControllerTracingNode { var onReady: () -> Void = {} var onError: () -> Void = {} - init(context: AccountContext, controller: StickerPackScreenImpl, stickerPacks: [StickerPackReference], initialSelectedStickerPackIndex: Int, modalProgressUpdated: @escaping (CGFloat, ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, openMention: @escaping (String) -> Void) { + init(context: AccountContext, controller: StickerPackScreenImpl, stickerPacks: [StickerPackReference], initialSelectedStickerPackIndex: Int, modalProgressUpdated: @escaping (CGFloat, ContainedViewLayoutTransition) -> Void, dismissed: @escaping () -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, openMention: @escaping (String) -> Void) { self.context = context self.controller = controller self.presentationData = controller.presentationData @@ -1302,7 +1302,7 @@ public final class StickerPackScreenImpl: ViewController { private let stickerPacks: [StickerPackReference] private let initialSelectedStickerPackIndex: Int fileprivate weak var parentNavigationController: NavigationController? - private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private var controllerNode: StickerPackScreenNode { return self.displayNode as! StickerPackScreenNode @@ -1320,7 +1320,7 @@ public final class StickerPackScreenImpl: ViewController { private var alreadyDidAppear: Bool = false - public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, stickerPacks: [StickerPackReference], selectedStickerPackIndex: Int = 0, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) -> Void)? = nil) { + public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, stickerPacks: [StickerPackReference], selectedStickerPackIndex: Int = 0, parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) -> Void)? = nil) { self.context = context self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } self.stickerPacks = stickerPacks @@ -1482,7 +1482,7 @@ public enum StickerPackScreenPerformedAction { case remove(positionInList: Int) } -public func StickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: StickerPackPreviewControllerMode = .default, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) -> Void)? = nil, dismissed: (() -> Void)? = nil) -> ViewController { +public func StickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, mode: StickerPackPreviewControllerMode = .default, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], parentNavigationController: NavigationController? = nil, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil, actionPerformed: ((StickerPackCollectionInfo, [StickerPackItem], StickerPackScreenPerformedAction) -> Void)? = nil, dismissed: (() -> Void)? = nil) -> ViewController { let stickerPacks = [mainStickerPack] let controller = StickerPackScreenImpl(context: context, stickerPacks: stickerPacks, selectedStickerPackIndex: stickerPacks.firstIndex(of: mainStickerPack) ?? 0, parentNavigationController: parentNavigationController, sendSticker: sendSticker, actionPerformed: actionPerformed) controller.dismissed = dismissed diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift index 8019bfe7a7..7e3d8915e3 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatJoinScreen.swift @@ -362,7 +362,7 @@ public final class VoiceChatJoinScreen: ViewController { let animation = contentNode.layer.makeAnimation(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.35) animation.fillMode = .both if !fastOut { - animation.beginTime = CACurrentMediaTime() + 0.1 + animation.beginTime = contentNode.layer.convertTime(CACurrentMediaTime(), from: nil) + 0.1 } contentNode.layer.add(animation, forKey: "opacity") diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 22e7126343..371cc76040 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -282,10 +282,11 @@ swift_library( "//submodules/TelegramUI/Components/AudioWaveformComponent:AudioWaveformComponent", "//submodules/TelegramUI/Components/EditableChatTextNode:EditableChatTextNode", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", - "//submodules/TelegramUI/Components/EmojiKeyboard:EmojiKeyboard", + "//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC", "//submodules/Media/LocalAudioTranscription:LocalAudioTranscription", ] + select({ diff --git a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift index 819d57826a..3201000137 100644 --- a/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift +++ b/submodules/TelegramUI/Components/AnimationCache/Sources/AnimationCache.swift @@ -41,9 +41,19 @@ public protocol AnimationCacheItemWriter: AnyObject { func finish() } +public final class AnimationCacheItemResult { + public let item: AnimationCacheItem? + public let isFinal: Bool + + public init(item: AnimationCacheItem?, isFinal: Bool) { + self.item = item + self.isFinal = isFinal + } +} + public protocol AnimationCache: AnyObject { - func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Signal - func getSynchronously(sourceId: String) -> AnimationCacheItem? + func get(sourceId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Signal + func getSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? } private func md5Hash(_ string: String) -> String { @@ -278,7 +288,7 @@ private func loadItem(path: String) -> AnimationCacheItem? { public final class AnimationCacheImpl: AnimationCache { private final class Impl { private final class ItemContext { - let subscribers = Bag<(AnimationCacheItem?) -> Void>() + let subscribers = Bag<(AnimationCacheItemResult) -> Void>() let disposable = MetaDisposable() deinit { @@ -301,13 +311,13 @@ public final class AnimationCacheImpl: AnimationCache { deinit { } - func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable, completion: @escaping (AnimationCacheItem?) -> Void) -> Disposable { - let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId)) + func get(sourceId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, updateResult: @escaping (AnimationCacheItemResult) -> Void) -> Disposable { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))")) let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" if FileManager.default.fileExists(atPath: itemPath) { - completion(loadItem(path: itemPath)) + updateResult(AnimationCacheItemResult(item: loadItem(path: itemPath), isFinal: true)) return EmptyDisposable } @@ -323,7 +333,9 @@ public final class AnimationCacheImpl: AnimationCache { } let queue = self.queue - let index = itemContext.subscribers.add(completion) + let index = itemContext.subscribers.add(updateResult) + + updateResult(AnimationCacheItemResult(item: nil, isFinal: false)) if beginFetch { let tempPath = self.allocateTempFile() @@ -349,14 +361,14 @@ public final class AnimationCacheImpl: AnimationCache { } for f in itemContext.subscribers.copyItems() { - f(item) + f(AnimationCacheItemResult(item: item, isFinal: true)) } } }) else { return EmptyDisposable } - let fetchDisposable = fetch(writer) + let fetchDisposable = fetch(size, writer) itemContext.disposable.set(ActionDisposable { fetchDisposable.dispose() @@ -377,8 +389,8 @@ public final class AnimationCacheImpl: AnimationCache { } } - func getSynchronously(sourceId: String) -> AnimationCacheItem? { - let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId)) + func getSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { + let sourceIdPath = itemSubpath(hashString: md5Hash(sourceId + "-\(Int(size.width))x\(Int(size.height))")) let itemDirectoryPath = "\(self.basePath)/\(sourceIdPath.directory)" let itemPath = "\(itemDirectoryPath)/\(sourceIdPath.fileName)" @@ -401,14 +413,16 @@ public final class AnimationCacheImpl: AnimationCache { }) } - public func get(sourceId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Signal { + public func get(sourceId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.impl.with { impl in - disposable.set(impl.get(sourceId: sourceId, fetch: fetch, completion: { result in + disposable.set(impl.get(sourceId: sourceId, size: size, fetch: fetch, updateResult: { result in subscriber.putNext(result) - subscriber.putCompletion() + if result.isFinal { + subscriber.putCompletion() + } })) } @@ -417,9 +431,9 @@ public final class AnimationCacheImpl: AnimationCache { |> runOn(self.queue) } - public func getSynchronously(sourceId: String) -> AnimationCacheItem? { + public func getSynchronously(sourceId: String, size: CGSize) -> AnimationCacheItem? { return self.impl.syncWith { impl -> AnimationCacheItem? in - return impl.getSynchronously(sourceId: sourceId) + return impl.getSynchronously(sourceId: sourceId, size: size) } } } diff --git a/submodules/TelegramUI/Components/EmojiKeyboard/Sources/EmojiKeyboard.swift b/submodules/TelegramUI/Components/EmojiKeyboard/Sources/EmojiKeyboard.swift deleted file mode 100644 index 2cc37ab552..0000000000 --- a/submodules/TelegramUI/Components/EmojiKeyboard/Sources/EmojiKeyboard.swift +++ /dev/null @@ -1,4 +0,0 @@ -import Foundation -import UIKit -import Display - diff --git a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 700adef954..051259f600 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -46,16 +46,19 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { super.init() + let scale = min(2.0, UIScreenScale) + let pixelSize = CGSize(width: 24 * scale, height: 24 * scale) + if attemptSynchronousLoad { - if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation) { - let size = CGSize(width: 24.0, height: 24.0) + if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { + let size = CGSize(width: pixelSize.width / scale, height: pixelSize.height / scale) if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { self.contents = image.cgImage } } } - self.disposable = renderer.add(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, fetch: { writer in + self.disposable = renderer.add(groupId: groupId, target: self, 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) let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in @@ -67,8 +70,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { writer.finish() return } - let scale = min(2.0, UIScreenScale) - cacheLottieAnimation(data: data, width: Int(24 * scale), height: Int(24 * scale), writer: writer) + cacheLottieAnimation(data: data, width: Int(size.width), height: Int(size.height), writer: writer) }) let fetchDisposable = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start() diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD new file mode 100644 index 0000000000..254b799425 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -0,0 +1,35 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "EntityKeyboard", + module_name = "EntityKeyboard", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Components/PagerComponent:PagerComponent", + "//submodules/Components/BlurredBackgroundComponent:BlurredBackgroundComponent", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + "//submodules/Components/BundleIconComponent:BundleIconComponent", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TelegramCore:TelegramCore", + "//submodules/Postbox:Postbox", + "//submodules/AnimatedStickerNode:AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", + "//submodules/YuvConversion:YuvConversion", + "//submodules/AccountContext:AccountContext", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/ShimmerEffect:ShimmerEffect", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift new file mode 100644 index 0000000000..0c684fa098 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -0,0 +1,631 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox +import MultiAnimationRenderer +import AnimationCache +import AccountContext +import LottieAnimationCache +import AnimatedStickerNode +import TelegramAnimatedStickerNode +import SwiftSignalKit +import ShimmerEffect +import PagerComponent + +public final class EmojiPagerContentComponent: Component { + public typealias EnvironmentType = (EntityKeyboardChildEnvironment, PagerComponentChildEnvironment) + + public final class InputInteraction { + public let performItemAction: (Item, UIView, CGRect) -> Void + public let deleteBackwards: () -> Void + + public init( + performItemAction: @escaping (Item, UIView, CGRect) -> Void, + deleteBackwards: @escaping () -> Void + ) { + self.performItemAction = performItemAction + self.deleteBackwards = deleteBackwards + } + } + + public final class Item: Equatable { + public let emoji: String + public let file: TelegramMediaFile + + public init(emoji: String, file: TelegramMediaFile) { + self.emoji = emoji + self.file = file + } + + public static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs === rhs { + return true + } + if lhs.emoji != rhs.emoji { + return false + } + if lhs.file.fileId != rhs.file.fileId { + return false + } + + return true + } + } + + public final class ItemGroup: Equatable { + public let id: AnyHashable + public let title: String? + public let items: [Item] + + public init( + id: AnyHashable, + title: String?, + items: [Item] + ) { + self.id = id + self.title = title + self.items = items + } + + public static func ==(lhs: ItemGroup, rhs: ItemGroup) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.title != rhs.title { + return false + } + if lhs.items != rhs.items { + return false + } + return true + } + } + + public enum ItemLayoutType { + case compact + case detailed + } + + public let context: AccountContext + public let animationCache: AnimationCache + public let animationRenderer: MultiAnimationRenderer + public let inputInteraction: InputInteraction + public let itemGroups: [ItemGroup] + public let itemLayoutType: ItemLayoutType + + public init( + context: AccountContext, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + inputInteraction: InputInteraction, + itemGroups: [ItemGroup], + itemLayoutType: ItemLayoutType + ) { + self.context = context + self.animationCache = animationCache + self.animationRenderer = animationRenderer + self.inputInteraction = inputInteraction + self.itemGroups = itemGroups + self.itemLayoutType = itemLayoutType + } + + public static func ==(lhs: EmojiPagerContentComponent, rhs: EmojiPagerContentComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.animationCache !== rhs.animationCache { + return false + } + if lhs.animationRenderer !== rhs.animationRenderer { + return false + } + if lhs.inputInteraction !== rhs.inputInteraction { + return false + } + if lhs.itemGroups != rhs.itemGroups { + return false + } + if lhs.itemLayoutType != rhs.itemLayoutType { + return false + } + + return true + } + + public final class View: UIView, UIScrollViewDelegate { + private struct ItemGroupDescription: Equatable { + let hasTitle: Bool + let itemCount: Int + } + + private struct ItemGroupLayout: Equatable { + let frame: CGRect + let itemTopOffset: CGFloat + let itemCount: Int + } + + private struct ItemLayout: Equatable { + var width: CGFloat + var containerInsets: UIEdgeInsets + var itemGroupLayouts: [ItemGroupLayout] + var itemSize: CGFloat + var horizontalSpacing: CGFloat + var verticalSpacing: CGFloat + var verticalGroupSpacing: CGFloat + var itemsPerRow: Int + var contentSize: CGSize + + init(width: CGFloat, containerInsets: UIEdgeInsets, itemGroups: [ItemGroupDescription], itemLayoutType: ItemLayoutType) { + self.width = width + self.containerInsets = containerInsets + + let minSpacing: CGFloat + switch itemLayoutType { + case .compact: + self.itemSize = 36.0 + self.verticalSpacing = 9.0 + minSpacing = 9.0 + case .detailed: + self.itemSize = 60.0 + self.verticalSpacing = 9.0 + minSpacing = 9.0 + } + + self.verticalGroupSpacing = 18.0 + + let itemHorizontalSpace = width - self.containerInsets.left - self.containerInsets.right + + self.itemsPerRow = Int((itemHorizontalSpace + minSpacing) / (self.itemSize + minSpacing)) + self.horizontalSpacing = floor((itemHorizontalSpace - self.itemSize * CGFloat(self.itemsPerRow)) / CGFloat(self.itemsPerRow - 1)) + + var verticalGroupOrigin: CGFloat = self.containerInsets.top + self.itemGroupLayouts = [] + for itemGroup in itemGroups { + var itemTopOffset: CGFloat = 0.0 + if itemGroup.hasTitle { + itemTopOffset += 24.0 + } + + let numRowsInGroup = (itemGroup.itemCount + (self.itemsPerRow - 1)) / self.itemsPerRow + let groupContentSize = CGSize(width: width, height: itemTopOffset + CGFloat(numRowsInGroup) * self.itemSize + CGFloat(max(0, numRowsInGroup - 1)) * self.verticalSpacing) + self.itemGroupLayouts.append(ItemGroupLayout( + frame: CGRect(origin: CGPoint(x: 0.0, y: verticalGroupOrigin), size: groupContentSize), + itemTopOffset: itemTopOffset, + itemCount: itemGroup.itemCount + )) + verticalGroupOrigin += groupContentSize.height + self.verticalGroupSpacing + } + verticalGroupOrigin += self.containerInsets.bottom + self.contentSize = CGSize(width: width, height: verticalGroupOrigin) + } + + func frame(groupIndex: Int, itemIndex: Int) -> CGRect { + let groupLayout = self.itemGroupLayouts[groupIndex] + + let row = itemIndex / self.itemsPerRow + let column = itemIndex % self.itemsPerRow + + return CGRect( + origin: CGPoint( + x: self.containerInsets.left + CGFloat(column) * (self.itemSize + self.horizontalSpacing), + y: groupLayout.frame.minY + groupLayout.itemTopOffset + CGFloat(row) * (self.itemSize + self.verticalSpacing) + ), + size: CGSize( + width: self.itemSize, + height: self.itemSize + ) + ) + } + + func visibleItems(for rect: CGRect) -> [(groupIndex: Int, groupItems: Range)] { + var result: [(groupIndex: Int, groupItems: Range)] = [] + + for groupIndex in 0 ..< self.itemGroupLayouts.count { + let group = self.itemGroupLayouts[groupIndex] + + if !rect.intersects(group.frame) { + continue + } + let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -group.frame.minY - group.itemTopOffset) + var minVisibleRow = Int(floor((offsetRect.minY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) + minVisibleRow = max(0, minVisibleRow) + let maxVisibleRow = Int(ceil((offsetRect.maxY - self.verticalSpacing) / (self.itemSize + self.verticalSpacing))) + + let minVisibleIndex = minVisibleRow * self.itemsPerRow + let maxVisibleIndex = min(group.itemCount - 1, (maxVisibleRow + 1) * self.itemsPerRow - 1) + + if maxVisibleIndex >= minVisibleIndex { + result.append(( + groupIndex: groupIndex, + groupItems: minVisibleIndex ..< (maxVisibleIndex + 1) + )) + } + } + + return result + } + } + + final class ItemLayer: MultiAnimationRenderTarget { + let item: Item + + private let file: TelegramMediaFile + private let placeholderColor: UIColor + private let size: CGSize + private var disposable: Disposable? + private var fetchDisposable: Disposable? + + private var isInHierarchyValue: Bool = false + public var isVisibleForAnimations: Bool = false { + didSet { + if self.isVisibleForAnimations != oldValue { + self.updatePlayback() + } + } + } + private var displayPlaceholder: Bool = false + + init( + item: Item, + context: AccountContext, + groupId: String, + attemptSynchronousLoad: Bool, + file: TelegramMediaFile, + cache: AnimationCache, + renderer: MultiAnimationRenderer, + placeholderColor: UIColor, + pointSize: CGSize + ) { + self.item = item + self.file = file + self.placeholderColor = placeholderColor + + let scale = min(2.0, UIScreenScale) + let pixelSize = CGSize(width: pointSize.width * scale, height: pointSize.height * scale) + self.size = CGSize(width: pixelSize.width / scale, height: pixelSize.height / scale) + + super.init() + + if attemptSynchronousLoad { + if !renderer.loadFirstFrameSynchronously(groupId: groupId, target: self, cache: cache, itemId: file.resource.id.stringRepresentation, size: pixelSize) { + self.displayPlaceholder = true + + if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: self.size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { + self.contents = image.cgImage + } + } + } + + self.disposable = renderer.add(groupId: groupId, target: self, 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) + + let dataDisposable = source.directDataPath(attemptSynchronously: false).start(next: { result in + guard let result = result else { + return + } + + 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 = freeMediaFileInteractiveFetched(account: context.account, fileReference: .standalone(media: file)).start() + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable.dispose() + } + }) + } + + override public init(layer: Any) { + preconditionFailure() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable?.dispose() + self.fetchDisposable?.dispose() + } + + override public func action(forKey event: String) -> CAAction? { + if event == kCAOnOrderIn { + self.isInHierarchyValue = true + } else if event == kCAOnOrderOut { + self.isInHierarchyValue = false + } + self.updatePlayback() + return nullAction + } + + private func updatePlayback() { + let shouldBePlaying = self.isInHierarchyValue && self.isVisibleForAnimations + + self.shouldBeAnimating = shouldBePlaying + } + + override func updateDisplayPlaceholder(displayPlaceholder: Bool) { + if self.displayPlaceholder == displayPlaceholder { + return + } + + self.displayPlaceholder = displayPlaceholder + let file = self.file + let size = self.size + let placeholderColor = self.placeholderColor + + Queue.concurrentDefaultQueue().async { [weak self] in + if let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: size, imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) { + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + + if strongSelf.displayPlaceholder { + strongSelf.contents = image.cgImage + } + } + } + } + } + } + + private let scrollView: UIScrollView + + private var visibleItemLayers: [MediaId: ItemLayer] = [:] + private var visibleGroupHeaders: [AnyHashable: ComponentHostView] = [:] + private var ignoreScrolling: Bool = false + + private var component: EmojiPagerContentComponent? + private var pagerEnvironment: PagerComponentChildEnvironment? + private var theme: PresentationTheme? + private var itemLayout: ItemLayout? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = 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.delegate = self + self.addSubview(self.scrollView) + + 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 { + if let component = self.component, let item = self.item(atPoint: recognizer.location(in: self)), let itemView = self.visibleItemLayers[item.file.fileId] { + component.inputInteraction.performItemAction(item, self, self.scrollView.convert(itemView.frame, to: self)) + } + } + } + + private func item(atPoint point: CGPoint) -> Item? { + let localPoint = self.convert(point, to: self.scrollView) + + for (_, itemLayer) in self.visibleItemLayers { + if itemLayer.frame.contains(localPoint) { + return itemLayer.item + } + } + + return nil + } + + private var previousScrollingOffset: CGFloat? + + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + if let presentation = scrollView.layer.presentation() { + scrollView.bounds = presentation.bounds + scrollView.layer.removeAllAnimations() + } + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + + self.updateVisibleItems(attemptSynchronousLoads: false) + + self.updateScrollingOffset(transition: .immediate) + } + + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { + if velocity.y != 0.0 { + targetContentOffset.pointee.y = self.snappedContentOffset(proposedOffset: targetContentOffset.pointee.y) + } + } + + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + if !decelerate { + self.snapScrollingOffsetToInsets() + } + } + + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + self.snapScrollingOffsetToInsets() + } + + private func updateScrollingOffset(transition: Transition) { + if let previousScrollingOffsetValue = self.previousScrollingOffset { + let currentBounds = scrollView.bounds + let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0) + let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) + let offsetToClosestEdge = min(offsetToTopEdge, offsetToBottomEdge) + + let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue + self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate( + relativeOffset: relativeOffset, + absoluteOffsetToClosestEdge: offsetToClosestEdge, + transition: transition + )) + self.previousScrollingOffset = scrollView.contentOffset.y + } + self.previousScrollingOffset = scrollView.contentOffset.y + } + + private func snappedContentOffset(proposedOffset: CGFloat) -> CGFloat { + guard let pagerEnvironment = self.pagerEnvironment else { + return proposedOffset + } + + var proposedOffset = proposedOffset + let bounds = self.bounds + if proposedOffset + bounds.height > self.scrollView.contentSize.height - pagerEnvironment.containerInsets.bottom { + proposedOffset = self.scrollView.contentSize.height - bounds.height + } + if proposedOffset < pagerEnvironment.containerInsets.top { + proposedOffset = 0.0 + } + + return proposedOffset + } + + private func snapScrollingOffsetToInsets() { + let transition = Transition(animation: .curve(duration: 0.4, curve: .spring)) + + var currentBounds = self.scrollView.bounds + currentBounds.origin.y = self.snappedContentOffset(proposedOffset: currentBounds.minY) + transition.setBounds(view: self.scrollView, bounds: currentBounds) + + self.updateScrollingOffset(transition: transition) + } + + private func updateVisibleItems(attemptSynchronousLoads: Bool) { + guard let component = self.component, let theme = self.theme, let itemLayout = self.itemLayout else { + return + } + + var validIds = Set() + var validGroupHeaderIds = Set() + + for groupItems in itemLayout.visibleItems(for: self.scrollView.bounds) { + let itemGroup = component.itemGroups[groupItems.groupIndex] + let itemGroupLayout = itemLayout.itemGroupLayouts[groupItems.groupIndex] + + if let title = itemGroup.title { + validGroupHeaderIds.insert(itemGroup.id) + let groupHeaderView: ComponentHostView + if let current = self.visibleGroupHeaders[itemGroup.id] { + groupHeaderView = current + } else { + groupHeaderView = ComponentHostView() + self.visibleGroupHeaders[itemGroup.id] = groupHeaderView + self.scrollView.addSubview(groupHeaderView) + } + let groupHeaderSize = groupHeaderView.update( + transition: .immediate, + component: AnyComponent(Text( + text: title, 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) + ) + groupHeaderView.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 = item.file.fileId + validIds.insert(itemId) + + let itemLayer: ItemLayer + if let current = self.visibleItemLayers[itemId] { + itemLayer = current + } else { + itemLayer = ItemLayer(item: item, context: component.context, groupId: "keyboard", attemptSynchronousLoad: attemptSynchronousLoads, file: item.file, cache: component.animationCache, renderer: component.animationRenderer, placeholderColor: theme.chat.inputMediaPanel.stickersBackgroundColor, pointSize: CGSize(width: itemLayout.itemSize, height: itemLayout.itemSize)) + self.scrollView.layer.addSublayer(itemLayer) + self.visibleItemLayers[itemId] = itemLayer + } + + itemLayer.frame = itemLayout.frame(groupIndex: groupItems.groupIndex, itemIndex: index) + itemLayer.isVisibleForAnimations = true + } + } + + var removedIds: [MediaId] = [] + for (id, itemLayer) in self.visibleItemLayers { + if !validIds.contains(id) { + removedIds.append(id) + itemLayer.removeFromSuperlayer() + } + } + for id in removedIds { + self.visibleItemLayers.removeValue(forKey: id) + } + + var removedGroupHeaderIds: [AnyHashable] = [] + for (id, groupHeaderView) in self.visibleGroupHeaders { + if !validGroupHeaderIds.contains(id) { + removedGroupHeaderIds.append(id) + groupHeaderView.removeFromSuperview() + } + } + for id in removedGroupHeaderIds { + self.visibleGroupHeaders.removeValue(forKey: id) + } + } + + func update(component: EmojiPagerContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.theme = environment[EntityKeyboardChildEnvironment.self].value.theme + + let pagerEnvironment = environment[PagerComponentChildEnvironment.self].value + self.pagerEnvironment = pagerEnvironment + + var itemGroups: [ItemGroupDescription] = [] + for itemGroup in component.itemGroups { + itemGroups.append(ItemGroupDescription( + hasTitle: itemGroup.title != nil, + 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) + self.itemLayout = itemLayout + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize)) + if self.scrollView.contentSize != itemLayout.contentSize { + self.scrollView.contentSize = itemLayout.contentSize + } + self.previousScrollingOffset = self.scrollView.contentOffset.y + self.ignoreScrolling = false + + self.updateVisibleItems(attemptSynchronousLoads: true) + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public 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) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift new file mode 100644 index 0000000000..ae928d87ee --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -0,0 +1,214 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox +import BlurredBackgroundComponent +import BundleIconComponent + +public final class EntityKeyboardChildEnvironment: Equatable { + public let theme: PresentationTheme + + public init(theme: PresentationTheme) { + self.theme = theme + } + + public static func ==(lhs: EntityKeyboardChildEnvironment, rhs: EntityKeyboardChildEnvironment) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + + return true + } +} + +public final class EntityKeyboardComponent: Component { + public let theme: PresentationTheme + public let bottomInset: CGFloat + public let emojiContent: EmojiPagerContentComponent + public let stickerContent: EmojiPagerContentComponent + public let externalTopPanelContainer: UIView? + public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void + + public init( + theme: PresentationTheme, + bottomInset: CGFloat, + emojiContent: EmojiPagerContentComponent, + stickerContent: EmojiPagerContentComponent, + externalTopPanelContainer: UIView?, + topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void + ) { + self.theme = theme + self.bottomInset = bottomInset + self.emojiContent = emojiContent + self.stickerContent = stickerContent + self.externalTopPanelContainer = externalTopPanelContainer + self.topPanelExtensionUpdated = topPanelExtensionUpdated + } + + public static func ==(lhs: EntityKeyboardComponent, rhs: EntityKeyboardComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } + if lhs.emojiContent != rhs.emojiContent { + return false + } + if lhs.stickerContent != rhs.stickerContent { + return false + } + if lhs.externalTopPanelContainer != rhs.externalTopPanelContainer { + return false + } + + return true + } + + public final class View: UIView { + private let pagerView: ComponentHostView + + private var component: EntityKeyboardComponent? + + override init(frame: CGRect) { + self.pagerView = ComponentHostView() + + super.init(frame: frame) + + self.clipsToBounds = true + + self.addSubview(self.pagerView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EntityKeyboardComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + var contents: [AnyComponentWithIdentity<(EntityKeyboardChildEnvironment, PagerComponentChildEnvironment)>] = [] + var contentTopPanels: [AnyComponentWithIdentity] = [] + var contentIcons: [AnyComponentWithIdentity] = [] + var contentAccessoryRightButtons: [AnyComponentWithIdentity] = [] + + var topStickertems: [EntityKeyboardTopPanelComponent.Item] = [] + for itemGroup in component.stickerContent.itemGroups { + if !itemGroup.items.isEmpty { + topStickertems.append(EntityKeyboardTopPanelComponent.Item( + id: AnyHashable(itemGroup.items[0].file.fileId), + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: component.stickerContent.context, + file: itemGroup.items[0].file, + animationCache: component.stickerContent.animationCache, + animationRenderer: component.stickerContent.animationRenderer + )) + )) + } + } + contents.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(component.stickerContent))) + contentTopPanels.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(EntityKeyboardTopPanelComponent( + theme: component.theme, + items: topStickertems + )))) + contentIcons.append(AnyComponentWithIdentity(id: "stickers", component: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputStickersIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )))) + contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "stickers", 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))))) + + contents.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(component.emojiContent))) + var topEmojiItems: [EntityKeyboardTopPanelComponent.Item] = [] + for itemGroup in component.emojiContent.itemGroups { + if !itemGroup.items.isEmpty { + topEmojiItems.append(EntityKeyboardTopPanelComponent.Item( + id: AnyHashable(itemGroup.items[0].file.fileId), + content: AnyComponent(EntityKeyboardAnimationTopPanelComponent( + context: component.emojiContent.context, + file: itemGroup.items[0].file, + animationCache: component.emojiContent.animationCache, + animationRenderer: component.emojiContent.animationRenderer + )) + )) + } + } + + contentTopPanels.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(EntityKeyboardTopPanelComponent( + theme: component.theme, + items: topEmojiItems + )))) + contentIcons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputEmojiIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )))) + contentAccessoryRightButtons.append(AnyComponentWithIdentity(id: "emoji", component: AnyComponent(Button( + content: AnyComponent(BundleIconComponent( + name: "Chat/Input/Media/EntityInputClearIcon", + tintColor: component.theme.chat.inputMediaPanel.panelIconColor, + maxSize: nil + )), + action: { + component.emojiContent.inputInteraction.deleteBackwards() + } + ).minSize(CGSize(width: 38.0, height: 38.0))))) + + let pagerSize = self.pagerView.update( + transition: transition, + component: AnyComponent(PagerComponent( + contentInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0), + contents: contents, + contentTopPanels: contentTopPanels, + contentIcons: contentIcons, + contentAccessoryRightButtons: contentAccessoryRightButtons, + defaultId: "emoji", + contentBackground: AnyComponent(BlurredBackgroundComponent( + color: component.theme.chat.inputMediaPanel.stickersBackgroundColor.withMultipliedAlpha(0.75) + )), + topPanel: AnyComponent(EntityKeyboardTopContainerPanelComponent( + theme: component.theme + )), + externalTopPanelContainer: component.externalTopPanelContainer, + bottomPanel: AnyComponent(EntityKeyboardBottomPanelComponent( + theme: component.theme, + bottomInset: component.bottomInset, + deleteBackwards: { [weak self] in + self?.component?.emojiContent.inputInteraction.deleteBackwards() + } + )), + panelStateUpdated: { panelState, transition in + component.topPanelExtensionUpdated(panelState.topPanelHeight, transition) + } + )), + environment: { + EntityKeyboardChildEnvironment(theme: component.theme) + }, + containerSize: availableSize + ) + transition.setFrame(view: self.pagerView, frame: CGRect(origin: CGPoint(), size: pagerSize)) + + self.component = component + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public 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) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift new file mode 100644 index 0000000000..82a2ec6b70 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardBottomPanelComponent.swift @@ -0,0 +1,293 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox +import ComponentDisplayAdapters +import BundleIconComponent + +private final class BottomPanelIconComponent: Component { + let content: AnyComponent + + init(content: AnyComponent) { + self.content = content + } + + static func ==(lhs: BottomPanelIconComponent, rhs: BottomPanelIconComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + + return true + } + + final class View: UIView { + let contentView: ComponentHostView + + override init(frame: CGRect) { + self.contentView = ComponentHostView() + + super.init(frame: frame) + + self.addSubview(self.contentView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: BottomPanelIconComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let size = CGSize(width: 32.0, height: 32.0) + + let contentSize = self.contentView.update( + transition: transition, + component: component.content, + environment: {}, + containerSize: size + ) + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(x: floor((size.width - contentSize.width) / 2.0), y: (size.height - contentSize.height) / 2.0), size: contentSize)) + + return size + } + } + + 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 EntityKeyboardBottomPanelComponent: Component { + typealias EnvironmentType = PagerComponentPanelEnvironment + + let theme: PresentationTheme + let bottomInset: CGFloat + let deleteBackwards: () -> Void + + init( + theme: PresentationTheme, + bottomInset: CGFloat, + deleteBackwards: @escaping () -> Void + ) { + self.theme = theme + self.bottomInset = bottomInset + self.deleteBackwards = deleteBackwards + } + + static func ==(lhs: EntityKeyboardBottomPanelComponent, rhs: EntityKeyboardBottomPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } + + return true + } + + final class View: UIView { + private final class AccessoryButtonView { + let id: AnyHashable + let view: ComponentHostView + + init(id: AnyHashable, view: ComponentHostView) { + self.id = id + self.view = view + } + } + + private let backgroundView: BlurredBackgroundView + private let separatorView: UIView + private var rightAccessoryButton: AccessoryButtonView? + + private var iconViews: [AnyHashable: ComponentHostView] = [:] + private var highlightedIconBackgroundView: UIView + + private var component: EntityKeyboardBottomPanelComponent? + + override init(frame: CGRect) { + self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) + + self.separatorView = UIView() + self.separatorView.isUserInteractionEnabled = false + + self.highlightedIconBackgroundView = UIView() + self.highlightedIconBackgroundView.isUserInteractionEnabled = false + self.highlightedIconBackgroundView.layer.cornerRadius = 10.0 + self.highlightedIconBackgroundView.clipsToBounds = true + + super.init(frame: frame) + + self.addSubview(self.backgroundView) + self.addSubview(self.highlightedIconBackgroundView) + self.addSubview(self.separatorView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EntityKeyboardBottomPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.component?.theme !== component.theme { + self.separatorView.backgroundColor = component.theme.chat.inputMediaPanel.panelSeparatorColor + self.backgroundView.updateColor(color: component.theme.chat.inputPanel.panelBackgroundColor.withMultipliedAlpha(1.0), transition: .immediate) + self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor + } + + let intrinsicHeight: CGFloat = 38.0 + let height = intrinsicHeight + component.bottomInset + + let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value + let activeContentId = panelEnvironment.activeContentId + + var rightAccessoryButtonComponent: AnyComponentWithIdentity? + for contentAccessoryRightButton in panelEnvironment.contentAccessoryRightButtons { + if contentAccessoryRightButton.id == activeContentId { + rightAccessoryButtonComponent = contentAccessoryRightButton + break + } + } + let previousRightAccessoryButton = self.rightAccessoryButton + + if let rightAccessoryButtonComponent = rightAccessoryButtonComponent { + var rightAccessoryButtonTransition = transition + let rightAccessoryButton: AccessoryButtonView + if let current = self.rightAccessoryButton, current.id == rightAccessoryButtonComponent.id { + rightAccessoryButton = current + } else { + rightAccessoryButtonTransition = .immediate + rightAccessoryButton = AccessoryButtonView(id: rightAccessoryButtonComponent.id, view: ComponentHostView()) + self.rightAccessoryButton = rightAccessoryButton + self.addSubview(rightAccessoryButton.view) + } + + let rightAccessoryButtonSize = rightAccessoryButton.view.update( + transition: rightAccessoryButtonTransition, + component: AnyComponent(Button( + content: rightAccessoryButtonComponent.component, + action: { [weak self] in + self?.component?.deleteBackwards() + } + ).minSize(CGSize(width: intrinsicHeight, height: intrinsicHeight))), + environment: {}, + containerSize: CGSize(width: .greatestFiniteMagnitude, height: intrinsicHeight) + ) + rightAccessoryButtonTransition.setFrame(view: rightAccessoryButton.view, frame: CGRect(origin: CGPoint(x: availableSize.width - 2.0 - rightAccessoryButtonSize.width, y: 2.0), size: rightAccessoryButtonSize)) + } else { + self.rightAccessoryButton = nil + } + + if previousRightAccessoryButton !== self.rightAccessoryButton?.view { + if case .none = transition.animation { + previousRightAccessoryButton?.view.removeFromSuperview() + } else { + if let previousRightAccessoryButton = previousRightAccessoryButton { + let previousRightAccessoryButtonView = previousRightAccessoryButton.view + previousRightAccessoryButtonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) + previousRightAccessoryButtonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousRightAccessoryButtonView] _ in + previousRightAccessoryButtonView?.removeFromSuperview() + }) + } + + if let rightAccessoryButtonView = self.rightAccessoryButton?.view { + rightAccessoryButtonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.2) + rightAccessoryButtonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } + } + + var validIconIds: [AnyHashable] = [] + var iconInfos: [AnyHashable: (size: CGSize, transition: Transition)] = [:] + + var iconTotalSize = CGSize() + let iconSpacing: CGFloat = 22.0 + + for icon in panelEnvironment.contentIcons { + validIconIds.append(icon.id) + + var iconTransition = transition + let iconView: ComponentHostView + if let current = self.iconViews[icon.id] { + iconView = current + } else { + iconTransition = .immediate + iconView = ComponentHostView() + self.iconViews[icon.id] = iconView + self.addSubview(iconView) + } + + let iconSize = iconView.update( + transition: iconTransition, + component: AnyComponent(BottomPanelIconComponent( + content: icon.component + )), + environment: {}, + containerSize: CGSize(width: 32.0, height: 32.0) + ) + + iconInfos[icon.id] = (size: iconSize, transition: iconTransition) + + if !iconTotalSize.width.isZero { + iconTotalSize.width += iconSpacing + } + iconTotalSize.width += iconSize.width + iconTotalSize.height = max(iconTotalSize.height, iconSize.height) + } + + var nextIconOrigin = CGPoint(x: floor((availableSize.width - iconTotalSize.width) / 2.0), y: floor((intrinsicHeight - iconTotalSize.height) / 2.0) + 2.0) + for icon in panelEnvironment.contentIcons { + guard let iconInfo = iconInfos[icon.id], let iconView = self.iconViews[icon.id] else { + continue + } + + let iconFrame = CGRect(origin: nextIconOrigin, size: iconInfo.size) + iconInfo.transition.setFrame(view: iconView, frame: iconFrame, completion: nil) + + if let activeContentId = activeContentId, activeContentId == icon.id { + self.highlightedIconBackgroundView.isHidden = false + transition.setFrame(view: self.highlightedIconBackgroundView, frame: iconFrame) + } + + nextIconOrigin.x += iconInfo.size.width + iconSpacing + } + + if activeContentId == nil { + self.highlightedIconBackgroundView.isHidden = true + } + + var removedIconViewIds: [AnyHashable] = [] + for (id, iconView) in self.iconViews { + if !validIconIds.contains(id) { + removedIconViewIds.append(id) + iconView.removeFromSuperview() + } + } + for id in removedIconViewIds { + self.iconViews.removeValue(forKey: id) + } + + transition.setFrame(view: self.separatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: UIScreenPixel))) + + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: height))) + self.backgroundView.update(size: CGSize(width: availableSize.width, height: height), transition: transition.containedViewLayoutTransition) + + self.component = component + + return CGSize(width: availableSize.width, height: height) + } + } + + 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) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift new file mode 100644 index 0000000000..875bdd0156 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopContainerPanelComponent.swift @@ -0,0 +1,136 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox + +final class EntityKeyboardTopContainerPanelComponent: Component { + typealias EnvironmentType = PagerComponentPanelEnvironment + + let theme: PresentationTheme + + init( + theme: PresentationTheme + ) { + self.theme = theme + } + + static func ==(lhs: EntityKeyboardTopContainerPanelComponent, rhs: EntityKeyboardTopContainerPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + + return true + } + + final class View: UIView { + private var panelViews: [AnyHashable: ComponentHostView] = [:] + + private var component: EntityKeyboardTopContainerPanelComponent? + private var panelEnvironment: PagerComponentPanelEnvironment? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EntityKeyboardTopContainerPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let intrinsicHeight: CGFloat = 41.0 + let height = intrinsicHeight + + let panelEnvironment = environment[PagerComponentPanelEnvironment.self].value + + var transitionOffsetFraction: CGFloat = 0.0 + if case .none = transition.animation { + } else if let previousPanelEnvironment = self.panelEnvironment, let previousActiveContentId = previousPanelEnvironment.activeContentId, let activeContentId = panelEnvironment.activeContentId, previousActiveContentId != activeContentId { + if let previousIndex = panelEnvironment.contentTopPanels.firstIndex(where: { $0.id == previousActiveContentId }), let index = panelEnvironment.contentTopPanels.firstIndex(where: { $0.id == activeContentId }), previousIndex != index { + if index < previousIndex { + transitionOffsetFraction = -1.0 + } else { + transitionOffsetFraction = 1.0 + } + } + } + + self.component = component + self.panelEnvironment = panelEnvironment + self.state = state + + var validPanelIds = Set() + let visibleBounds = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: intrinsicHeight)) + if let centralId = panelEnvironment.activeContentId, let centralIndex = panelEnvironment.contentTopPanels.firstIndex(where: { $0.id == centralId }) { + for index in 0 ..< panelEnvironment.contentTopPanels.count { + let panel = panelEnvironment.contentTopPanels[index] + let indexOffset = index - centralIndex + + let panelFrame = CGRect(origin: CGPoint(x: CGFloat(indexOffset) * availableSize.width, y: 0.0), size: CGSize(width: availableSize.width, height: intrinsicHeight)) + + let isInBounds = visibleBounds.intersects(panelFrame) + let isPartOfTransition: Bool + if !transitionOffsetFraction.isZero && self.panelViews[panel.id] != nil { + isPartOfTransition = true + } else { + isPartOfTransition = false + } + + if isInBounds || isPartOfTransition { + validPanelIds.insert(panel.id) + + var panelTransition = transition + let panelView: ComponentHostView + if let current = self.panelViews[panel.id] { + panelView = current + } else { + panelTransition = .immediate + panelView = ComponentHostView() + self.panelViews[panel.id] = panelView + self.addSubview(panelView) + } + + let _ = panelView.update( + transition: panelTransition, + component: panel.component, + environment: {}, + containerSize: panelFrame.size + ) + if isInBounds { + transition.animatePosition(view: panelView, from: CGPoint(x: transitionOffsetFraction * availableSize.width, y: 0.0), to: CGPoint(), additive: true, completion: nil) + } + panelTransition.setFrame(view: panelView, frame: panelFrame, completion: { [weak self] completed in + if isPartOfTransition && completed { + self?.state?.updated(transition: .immediate) + } + }) + } + } + } + var removedPanelIds: [AnyHashable] = [] + for (id, panelView) in self.panelViews { + if !validPanelIds.contains(id) { + removedPanelIds.append(id) + panelView.removeFromSuperview() + } + } + for id in removedPanelIds { + self.panelViews.removeValue(forKey: id) + } + + return CGSize(width: availableSize.width, height: height) + } + } + + 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) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift new file mode 100644 index 0000000000..9e7732f6c4 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -0,0 +1,315 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import PagerComponent +import TelegramPresentationData +import TelegramCore +import Postbox +import AnimationCache +import MultiAnimationRenderer +import AccountContext + +final class EntityKeyboardAnimationTopPanelComponent: Component { + typealias EnvironmentType = Empty + + let context: AccountContext + let file: TelegramMediaFile + let animationCache: AnimationCache + let animationRenderer: MultiAnimationRenderer + + init( + context: AccountContext, + file: TelegramMediaFile, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer + ) { + self.context = context + self.file = file + self.animationCache = animationCache + self.animationRenderer = animationRenderer + } + + static func ==(lhs: EntityKeyboardAnimationTopPanelComponent, rhs: EntityKeyboardAnimationTopPanelComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.file.fileId != rhs.file.fileId { + return false + } + if lhs.animationCache !== rhs.animationCache { + return false + } + if lhs.animationRenderer !== rhs.animationRenderer { + return false + } + + return true + } + + final class View: UIView { + var itemLayer: EmojiPagerContentComponent.View.ItemLayer? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: EntityKeyboardAnimationTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.itemLayer == nil { + let itemLayer = EmojiPagerContentComponent.View.ItemLayer( + item: EmojiPagerContentComponent.Item( + emoji: "", + file: component.file + ), + context: component.context, + groupId: "topPanel", + attemptSynchronousLoad: false, + file: component.file, + cache: component.animationCache, + renderer: component.animationRenderer, + placeholderColor: .lightGray, + pointSize: CGSize(width: 28.0, height: 28.0) + ) + self.itemLayer = itemLayer + self.layer.addSublayer(itemLayer) + itemLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: 28.0, height: 28.0)) + itemLayer.isVisibleForAnimations = true + } + + return CGSize(width: 28.0, height: 28.0) + } + } + + 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 EntityKeyboardTopPanelComponent: Component { + typealias EnvironmentType = Empty + + final class Item: Equatable { + let id: AnyHashable + let content: AnyComponent + + init(id: AnyHashable, content: AnyComponent) { + self.id = id + self.content = content + } + + static func ==(lhs: Item, rhs: Item) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.content != rhs.content { + return false + } + + return true + } + } + + let theme: PresentationTheme + let items: [Item] + + init( + theme: PresentationTheme, + items: [Item] + ) { + self.theme = theme + self.items = items + } + + static func ==(lhs: EntityKeyboardTopPanelComponent, rhs: EntityKeyboardTopPanelComponent) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.items != rhs.items { + return false + } + + return true + } + + final class View: UIView, UIScrollViewDelegate { + private struct ItemLayout { + let sideInset: CGFloat = 7.0 + let itemSize: CGFloat = 32.0 + let innerItemSize: CGFloat = 28.0 + let itemSpacing: CGFloat = 15.0 + let itemCount: Int + let contentSize: CGSize + + init(itemCount: Int) { + self.itemCount = itemCount + self.contentSize = CGSize(width: sideInset * 2.0 + CGFloat(itemCount) * self.itemSize + CGFloat(max(0, itemCount - 1)) * itemSpacing, height: 41.0) + } + + func containerFrame(at index: Int) -> CGRect { + return CGRect(origin: CGPoint(x: sideInset + CGFloat(index) * (self.itemSize + self.itemSpacing), y: floor((self.contentSize.height - self.itemSize) / 2.0)), size: CGSize(width: self.itemSize, height: self.itemSize)) + } + + func contentFrame(at index: Int) -> CGRect { + var frame = self.containerFrame(at: index) + frame.origin.x += floor((self.itemSize - self.innerItemSize)) / 2.0 + frame.origin.y += floor((self.itemSize - self.innerItemSize)) / 2.0 + frame.size = CGSize(width: self.innerItemSize, height: self.innerItemSize) + return frame + } + + 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 + self.itemSpacing))) + minVisibleColumn = max(0, minVisibleColumn) + let maxVisibleColumn = Int(ceil((offsetRect.maxX - self.itemSpacing) / (self.itemSize + self.itemSpacing))) + + let minVisibleIndex = minVisibleColumn + let maxVisibleIndex = min(maxVisibleColumn, self.itemCount - 1) + + return (minVisibleIndex, maxVisibleIndex) + } + } + + private let scrollView: UIScrollView + private var itemViews: [AnyHashable: ComponentHostView] = [:] + private var highlightedIconBackgroundView: UIView + + private var itemLayout: ItemLayout? + private var ignoreScrolling: Bool = false + + private var component: EntityKeyboardTopPanelComponent? + + override init(frame: CGRect) { + self.scrollView = UIScrollView() + + self.highlightedIconBackgroundView = UIView() + self.highlightedIconBackgroundView.isUserInteractionEnabled = false + self.highlightedIconBackgroundView.layer.cornerRadius = 10.0 + self.highlightedIconBackgroundView.clipsToBounds = true + + super.init(frame: frame) + + self.scrollView.delaysContentTouches = 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.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.addSubview(self.highlightedIconBackgroundView) + + self.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in + guard let strongSelf = self else { + return false + } + return strongSelf.scrollView.contentOffset.x > 0.0 + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + + self.updateVisibleItems(attemptSynchronousLoads: false) + } + + private func updateVisibleItems(attemptSynchronousLoads: Bool) { + guard let component = self.component, let itemLayout = self.itemLayout else { + return + } + + var validIds = Set() + let visibleItemRange = itemLayout.visibleItemRange(for: self.scrollView.bounds) + if !component.items.isEmpty && visibleItemRange.maxIndex >= visibleItemRange.minIndex { + for index in visibleItemRange.minIndex ... visibleItemRange.maxIndex { + let item = component.items[index] + validIds.insert(item.id) + + let itemView: ComponentHostView + if let current = self.itemViews[item.id] { + itemView = current + } else { + itemView = ComponentHostView() + self.scrollView.addSubview(itemView) + self.itemViews[item.id] = itemView + } + + let itemFrame = itemLayout.contentFrame(at: index) + itemView.frame = itemFrame + let _ = itemView.update( + transition: .immediate, + component: item.content, + environment: {}, + containerSize: itemFrame.size + ) + } + } + var removedIds: [AnyHashable] = [] + for (id, itemView) in self.itemViews { + if !validIds.contains(id) { + removedIds.append(id) + itemView.removeFromSuperview() + } + } + for id in removedIds { + self.itemViews.removeValue(forKey: id) + } + } + + func update(component: EntityKeyboardTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if self.component?.theme !== component.theme { + self.highlightedIconBackgroundView.backgroundColor = component.theme.chat.inputMediaPanel.panelHighlightedIconBackgroundColor + } + self.component = component + + let intrinsicHeight: CGFloat = 41.0 + let height = intrinsicHeight + + let itemLayout = ItemLayout(itemCount: component.items.count) + self.itemLayout = itemLayout + + self.ignoreScrolling = true + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: intrinsicHeight))) + if self.scrollView.contentSize != itemLayout.contentSize { + self.scrollView.contentSize = itemLayout.contentSize + } + self.ignoreScrolling = false + + if let _ = component.items.first { + self.highlightedIconBackgroundView.isHidden = false + let itemFrame = itemLayout.containerFrame(at: 0) + transition.setFrame(view: self.highlightedIconBackgroundView, frame: itemFrame) + } + + self.updateVisibleItems(attemptSynchronousLoads: true) + + return CGSize(width: availableSize.width, height: height) + } + } + + 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) + } +} diff --git a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift index 97fb1cee2c..d399ea98cc 100644 --- a/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift +++ b/submodules/TelegramUI/Components/MultiAnimationRenderer/Sources/MultiAnimationRenderer.swift @@ -5,8 +5,8 @@ import Display import AnimationCache public protocol MultiAnimationRenderer: AnyObject { - func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable - func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String) -> Bool + func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable + func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool } open class MultiAnimationRenderTarget: SimpleLayer { @@ -28,6 +28,9 @@ open class MultiAnimationRenderTarget: SimpleLayer { f() } } + + open func updateDisplayPlaceholder(displayPlaceholder: Bool) { + } } private func convertFrameToImage(frame: AnimationCacheItemFrame) -> UIImage? { @@ -126,6 +129,7 @@ private final class ItemAnimationContext { private var displayLink: ConstantDisplayLinkAnimator? private var frameIndex: Int = 0 private var item: AnimationCacheItem? + private var frameSkip: Int private var currentFrameGroup: FrameGroup? private var isLoadingFrameGroup: Bool = false @@ -140,17 +144,26 @@ private final class ItemAnimationContext { let targets = Bag>() - init(cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) { + init(cache: AnimationCache, itemId: String, size: CGSize, frameSkip: Int, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable, stateUpdated: @escaping () -> Void) { self.cache = cache + self.frameSkip = frameSkip self.stateUpdated = stateUpdated - self.disposable = cache.get(sourceId: itemId, fetch: fetch).start(next: { [weak self] item in + self.disposable = cache.get(sourceId: itemId, size: size, fetch: fetch).start(next: { [weak self] result in Queue.mainQueue().async { - guard let strongSelf = self, let item = item else { + guard let strongSelf = self else { return } - strongSelf.item = item + strongSelf.item = result.item strongSelf.updateIsPlaying() + + if result.item == nil { + for target in strongSelf.targets.copyItems() { + if let target = target.value { + target.updateDisplayPlaceholder(displayPlaceholder: true) + } + } + } } }) } @@ -165,6 +178,7 @@ private final class ItemAnimationContext { let currentFrame = self.frameIndex % item.numFrames if let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) { + target.updateDisplayPlaceholder(displayPlaceholder: false) target.contents = currentFrameGroup.image.cgImage target.contentsRect = contentsRect } @@ -210,11 +224,12 @@ private final class ItemAnimationContext { } else if !self.isLoadingFrameGroup { self.currentFrameGroup = nil self.isLoadingFrameGroup = true + let frameSkip = self.frameSkip return LoadFrameGroupTask(task: { [weak self] in let possibleCounts: [Int] = [10, 12, 14, 16, 18, 20] let countIndex = Int.random(in: 0 ..< possibleCounts.count) - let currentFrameGroup = FrameGroup(item: item, baseFrameIndex: currentFrame, count: possibleCounts[countIndex], skip: 2) + let currentFrameGroup = FrameGroup(item: item, baseFrameIndex: currentFrame, count: possibleCounts[countIndex], skip: frameSkip) return { guard let strongSelf = self else { @@ -236,12 +251,15 @@ private final class ItemAnimationContext { } if advanceFrame { - self.frameIndex += 2 + self.frameIndex += self.frameSkip } if let currentFrameGroup = self.currentFrameGroup, let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) { for target in self.targets.copyItems() { - target.value?.contentsRect = contentsRect + if let target = target.value { + target.updateDisplayPlaceholder(displayPlaceholder: false) + target.contentsRect = contentsRect + } } } @@ -251,6 +269,7 @@ private final class ItemAnimationContext { public final class MultiAnimationRendererImpl: MultiAnimationRenderer { private final class GroupContext { + private var frameSkip: Int private let stateUpdated: () -> Void private var itemContexts: [String: ItemAnimationContext] = [:] @@ -263,16 +282,17 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } } - init(stateUpdated: @escaping () -> Void) { + init(frameSkip: Int, stateUpdated: @escaping () -> Void) { + self.frameSkip = frameSkip self.stateUpdated = stateUpdated } - func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable { + func add(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { let itemContext: ItemAnimationContext if let current = self.itemContexts[itemId] { itemContext = current } else { - itemContext = ItemAnimationContext(cache: cache, itemId: itemId, fetch: fetch, stateUpdated: { [weak self] in + itemContext = ItemAnimationContext(cache: cache, itemId: itemId, size: size, frameSkip: self.frameSkip, fetch: fetch, stateUpdated: { [weak self] in guard let strongSelf = self else { return } @@ -318,8 +338,8 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } } - func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String) -> Bool { - if let item = cache.getSynchronously(sourceId: itemId) { + func loadFirstFrameSynchronously(target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { + if let item = cache.getSynchronously(sourceId: itemId, size: size) { guard let frameGroup = FrameGroup(item: item, baseFrameIndex: 0, count: 1, skip: 1) else { return false } @@ -359,6 +379,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } private var groupContexts: [String: GroupContext] = [:] + private var frameSkip: Int private var displayLink: ConstantDisplayLinkAnimator? private(set) var isPlaying: Bool = false { @@ -372,7 +393,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } strongSelf.animationTick() } - self.displayLink?.frameInterval = 2 + self.displayLink?.frameInterval = self.frameSkip self.displayLink?.isPaused = false } } else { @@ -386,14 +407,19 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { } public init() { + if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.activeProcessorCount > 2 { + self.frameSkip = 1 + } else { + self.frameSkip = 2 + } } - public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, fetch: @escaping (AnimationCacheItemWriter) -> Disposable) -> Disposable { + public func add(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize, fetch: @escaping (CGSize, AnimationCacheItemWriter) -> Disposable) -> Disposable { let groupContext: GroupContext if let current = self.groupContexts[groupId] { groupContext = current } else { - groupContext = GroupContext(stateUpdated: { [weak self] in + groupContext = GroupContext(frameSkip: self.frameSkip, stateUpdated: { [weak self] in guard let strongSelf = self else { return } @@ -402,19 +428,19 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { self.groupContexts[groupId] = groupContext } - let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, fetch: fetch) + let disposable = groupContext.add(target: target, cache: cache, itemId: itemId, size: size, fetch: fetch) return ActionDisposable { disposable.dispose() } } - public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String) -> Bool { + public func loadFirstFrameSynchronously(groupId: String, target: MultiAnimationRenderTarget, cache: AnimationCache, itemId: String, size: CGSize) -> Bool { let groupContext: GroupContext if let current = self.groupContexts[groupId] { groupContext = current } else { - groupContext = GroupContext(stateUpdated: { [weak self] in + groupContext = GroupContext(frameSkip: self.frameSkip, stateUpdated: { [weak self] in guard let strongSelf = self else { return } @@ -423,7 +449,7 @@ public final class MultiAnimationRendererImpl: MultiAnimationRenderer { self.groupContexts[groupId] = groupContext } - return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId) + return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size) } private func updateIsPlaying() { diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json new file mode 100644 index 0000000000..f93f8f8fb7 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_clear.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/ic_clear.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/ic_clear.pdf new file mode 100644 index 0000000000..559ad92734 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputClearIcon.imageset/ic_clear.pdf @@ -0,0 +1,107 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 3.180420 6.169983 cm +0.000000 0.000000 0.000000 scn +7.131522 16.439991 m +8.015658 17.225891 9.157463 17.660004 10.340398 17.660004 c +19.819601 17.660004 l +22.487137 17.660004 24.649601 15.497540 24.649601 12.830004 c +24.649601 4.830004 l +24.649601 2.162468 22.487137 0.000004 19.819601 0.000004 c +10.340399 0.000004 l +9.157463 0.000004 8.015657 0.434116 7.131521 1.220016 c +0.949850 6.714835 l +-0.316617 7.840584 -0.316617 9.819424 0.949850 10.945172 c +7.131522 16.439991 l +h +10.340398 16.000004 m +9.564020 16.000004 8.814637 15.715089 8.234365 15.199291 c +2.052694 9.704473 l +1.529102 9.239058 1.529102 8.420950 2.052694 7.955535 c +8.234365 2.460716 l +8.814637 1.944919 9.564020 1.660004 10.340399 1.660004 c +19.819601 1.660004 l +21.570343 1.660004 22.989601 3.079261 22.989601 4.830004 c +22.989601 12.830004 l +22.989601 14.580747 21.570343 16.000004 19.819601 16.000004 c +10.340398 16.000004 l +h +10.732702 12.916903 m +11.056838 13.241037 11.582364 13.241037 11.906500 12.916903 c +14.819601 10.003801 l +17.732702 12.916903 l +18.056837 13.241037 18.582365 13.241037 18.906500 12.916903 c +19.230635 12.592768 19.230635 12.067240 18.906500 11.743105 c +15.993399 8.830004 l +18.906500 5.916903 l +19.230635 5.592768 19.230635 5.067240 18.906500 4.743105 c +18.582365 4.418970 18.056837 4.418970 17.732702 4.743105 c +14.819601 7.656206 l +11.906500 4.743105 l +11.582364 4.418970 11.056838 4.418970 10.732702 4.743105 c +10.408567 5.067241 10.408567 5.592767 10.732702 5.916903 c +13.645803 8.830004 l +10.732702 11.743105 l +10.408567 12.067240 10.408567 12.592768 10.732702 12.916903 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1651 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001741 00000 n +0000001764 00000 n +0000001937 00000 n +0000002011 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2070 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputEmojiIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputEmojiIcon.imageset/Contents.json new file mode 100644 index 0000000000..8d1829694d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputEmojiIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_emoji.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputEmojiIcon.imageset/ic_emoji.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputEmojiIcon.imageset/ic_emoji.pdf new file mode 100644 index 0000000000..51dd673605 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputEmojiIcon.imageset/ic_emoji.pdf @@ -0,0 +1,123 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 7.292999 5.741058 cm +0.000000 0.000000 0.000000 scn +7.706251 7.052563 m +9.859218 7.052563 11.806032 7.518116 13.204300 7.852490 c +14.570075 8.179095 15.412500 8.380550 15.412500 7.900764 c +15.412500 3.644719 11.962295 0.194514 7.706251 0.194514 c +3.450206 0.194514 0.000000 3.644719 0.000000 7.900764 c +0.000000 8.380550 0.842425 8.179095 2.208200 7.852490 c +3.606467 7.518116 5.553283 7.052563 7.706251 7.052563 c +h +12.468611 6.348519 m +11.392131 6.066849 9.742197 5.635127 7.697002 5.635127 c +5.651808 5.635127 4.001874 6.066849 2.925394 6.348519 c +2.030582 6.582655 1.532002 6.713113 1.532002 6.405752 c +1.532002 3.455799 6.013958 3.323253 7.697002 3.323253 c +9.586575 3.323253 13.862001 3.456314 13.862001 6.405752 c +13.862001 6.713113 13.363423 6.582655 12.468611 6.348519 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 2.669983 2.669983 cm +0.000000 0.000000 0.000000 scn +12.330000 23.000004 m +6.437122 23.000004 1.660000 18.222881 1.660000 12.330004 c +1.660000 6.437126 6.437122 1.660004 12.330000 1.660004 c +18.222878 1.660004 23.000000 6.437126 23.000000 12.330004 c +23.000000 18.222881 18.222878 23.000004 12.330000 23.000004 c +h +0.000000 12.330004 m +0.000000 19.139675 5.520329 24.660004 12.330000 24.660004 c +19.139671 24.660004 24.660000 19.139675 24.660000 12.330004 c +24.660000 5.520332 19.139671 0.000004 12.330000 0.000004 c +5.520329 0.000004 0.000000 5.520332 0.000000 12.330004 c +h +f* +n +Q +q +1.000000 0.000000 -0.000000 1.000000 10.000000 16.250000 cm +0.000000 0.000000 0.000000 scn +3.000000 1.750000 m +3.000000 0.783502 2.328427 0.000000 1.500000 0.000000 c +0.671573 0.000000 0.000000 0.783502 0.000000 1.750000 c +0.000000 2.716498 0.671573 3.500000 1.500000 3.500000 c +2.328427 3.500000 3.000000 2.716498 3.000000 1.750000 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 17.000000 16.250000 cm +0.000000 0.000000 0.000000 scn +3.000000 1.750000 m +3.000000 0.783502 2.328427 0.000000 1.500000 0.000000 c +0.671573 0.000000 0.000000 0.783502 0.000000 1.750000 c +0.000000 2.716498 0.671573 3.500000 1.500000 3.500000 c +2.328427 3.500000 3.000000 2.716498 3.000000 1.750000 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 2167 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002257 00000 n +0000002280 00000 n +0000002453 00000 n +0000002527 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2586 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGifsIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGifsIcon.imageset/Contents.json new file mode 100644 index 0000000000..35222f8570 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGifsIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_gifs.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGifsIcon.imageset/ic_gifs.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGifsIcon.imageset/ic_gifs.pdf new file mode 100644 index 0000000000..410ff918fa --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputGifsIcon.imageset/ic_gifs.pdf @@ -0,0 +1,128 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.669983 2.670013 cm +0.000000 0.000000 0.000000 scn +12.330000 22.999989 m +6.437122 22.999989 1.660000 18.222866 1.660000 12.329988 c +1.660000 6.437111 6.437122 1.659988 12.330000 1.659988 c +18.222878 1.659988 23.000000 6.437111 23.000000 12.329988 c +23.000000 18.222866 18.222878 22.999989 12.330000 22.999989 c +h +0.000000 12.329988 m +0.000000 19.139660 5.520329 24.659988 12.330000 24.659988 c +19.139671 24.659988 24.660000 19.139660 24.660000 12.329988 c +24.660000 5.520317 19.139671 -0.000011 12.330000 -0.000011 c +5.520329 -0.000011 0.000000 5.520317 0.000000 12.329988 c +h +17.108725 15.659992 m +17.129999 15.659988 l +18.830000 15.659988 l +19.288397 15.659988 19.660000 15.288384 19.660000 14.829988 c +19.660000 14.371593 19.288397 13.999989 18.830000 13.999989 c +17.160000 13.999989 l +17.160000 13.159988 l +18.830000 13.159988 l +19.288397 13.159988 19.660000 12.788384 19.660000 12.329988 c +19.660000 11.871593 19.288397 11.499989 18.830000 11.499989 c +17.160000 11.499989 l +17.160000 9.829988 l +17.160000 9.371592 16.788397 8.999989 16.330000 8.999989 c +15.871604 8.999989 15.500000 9.371592 15.500000 9.829988 c +15.500000 12.329988 l +15.500000 14.029988 l +15.499996 14.051264 l +15.499962 14.172362 15.499924 14.307926 15.509568 14.425976 c +15.520575 14.560680 15.548159 14.743810 15.644961 14.933796 c +15.772472 15.184052 15.975937 15.387516 16.226192 15.515027 c +16.416180 15.611830 16.599308 15.639414 16.734013 15.650420 c +16.852062 15.660065 16.987627 15.660027 17.108725 15.659992 c +h +13.330000 15.659988 m +13.788396 15.659988 14.160000 15.288384 14.160000 14.829988 c +14.160000 9.829988 l +14.160000 9.371592 13.788396 8.999989 13.330000 8.999989 c +12.871604 8.999989 12.500000 9.371592 12.500000 9.829988 c +12.500000 14.829988 l +12.500000 15.288384 12.871604 15.659988 13.330000 15.659988 c +h +8.330000 15.659988 m +6.490891 15.659988 5.000000 14.169097 5.000000 12.329988 c +5.000000 10.490880 6.490891 8.999989 8.330000 8.999989 c +9.670162 8.999989 11.076591 9.880583 11.385777 11.507551 c +11.393431 11.547824 l +11.397078 11.588655 l +11.427523 11.929440 l +11.486586 12.590580 10.965777 13.159988 10.302004 13.159988 c +9.330000 13.159988 l +8.871603 13.159988 8.500000 12.788385 8.500000 12.329988 c +8.500000 11.871592 8.871603 11.499989 9.330000 11.499989 c +9.657213 11.499989 l +9.421404 10.962639 8.887941 10.659988 8.330000 10.659988 c +7.407684 10.659988 6.660000 11.407673 6.660000 12.329988 c +6.660000 13.252304 7.407684 13.999989 8.330000 13.999989 c +8.609308 13.999989 8.870320 13.932076 9.099771 13.812578 c +9.506336 13.600842 10.007568 13.758780 10.219306 14.165344 c +10.431044 14.571908 10.273106 15.073140 9.866541 15.284878 c +9.405935 15.524760 8.882543 15.659988 8.330000 15.659988 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 2783 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002873 00000 n +0000002896 00000 n +0000003069 00000 n +0000003143 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +3202 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/Contents.json new file mode 100644 index 0000000000..186f60078b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_search.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/ic_search.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/ic_search.pdf new file mode 100644 index 0000000000..1e6ad4f2b4 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSearchIcon.imageset/ic_search.pdf @@ -0,0 +1,82 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.999991 4.221268 cm +0.000000 0.000000 0.000000 scn +2.000000 12.278740 m +2.000000 15.868591 4.910149 18.778740 8.500000 18.778740 c +12.089850 18.778740 15.000000 15.868591 15.000000 12.278740 c +15.000000 8.688890 12.089850 5.778740 8.500000 5.778740 c +4.910149 5.778740 2.000000 8.688890 2.000000 12.278740 c +h +8.500000 20.778740 m +3.805580 20.778740 0.000000 16.973160 0.000000 12.278740 c +0.000000 7.584319 3.805580 3.778740 8.500000 3.778740 c +10.427133 3.778740 12.204475 4.420067 13.630243 5.500938 c +18.676220 0.454960 l +19.131182 0.000000 19.868818 0.000000 20.323780 0.454960 c +20.778740 0.909922 20.778740 1.647558 20.323780 2.102520 c +15.277802 7.148497 l +16.358673 8.574265 17.000000 10.351607 17.000000 12.278740 c +17.000000 16.973160 13.194421 20.778740 8.500000 20.778740 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 864 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000954 00000 n +0000000976 00000 n +0000001149 00000 n +0000001223 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1282 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json new file mode 100644 index 0000000000..6044f8aa11 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_tools.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/ic_tools.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/ic_tools.pdf new file mode 100644 index 0000000000..702905fc0c --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputSettingsIcon.imageset/ic_tools.pdf @@ -0,0 +1,177 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 2.459991 2.485321 cm +0.000000 0.000000 0.000000 scn +11.355469 0.000000 m +13.722656 0.000000 l +14.625000 0.000000 15.316406 0.550781 15.527344 1.417969 c +16.031250 3.609375 l +16.406250 3.738281 l +18.316406 2.566406 l +19.078125 2.085938 19.957031 2.203125 20.601562 2.847656 c +22.242188 4.476562 l +22.886719 5.121094 23.003906 6.011719 22.523438 6.761719 c +21.328125 8.660156 l +21.468750 9.011719 l +23.660156 9.527344 l +24.515625 9.738281 25.078125 10.441406 25.078125 11.332031 c +25.078125 13.652344 l +25.078125 14.542969 24.527344 15.246094 23.660156 15.457031 c +21.492188 15.984375 l +21.339844 16.359375 l +22.535156 18.257812 l +23.015625 19.007812 22.898438 19.886719 22.253906 20.542969 c +20.613281 22.183594 l +19.980469 22.816406 19.101562 22.933594 18.339844 22.464844 c +16.429688 21.292969 l +16.031250 21.445312 l +15.527344 23.636719 l +15.316406 24.503906 14.625000 25.054688 13.722656 25.054688 c +11.355469 25.054688 l +10.453125 25.054688 9.761719 24.503906 9.550781 23.636719 c +9.035156 21.445312 l +8.636719 21.292969 l +6.738281 22.464844 l +5.976562 22.933594 5.085938 22.816406 4.453125 22.183594 c +2.824219 20.542969 l +2.179688 19.886719 2.050781 19.007812 2.542969 18.257812 c +3.726562 16.359375 l +3.585938 15.984375 l +1.417969 15.457031 l +0.550781 15.246094 0.000000 14.542969 0.000000 13.652344 c +0.000000 11.332031 l +0.000000 10.441406 0.562500 9.738281 1.417969 9.527344 c +3.609375 9.011719 l +3.738281 8.660156 l +2.554688 6.761719 l +2.062500 6.011719 2.191406 5.121094 2.835938 4.476562 c +4.464844 2.847656 l +5.109375 2.203125 6.000000 2.085938 6.761719 2.566406 c +8.660156 3.738281 l +9.035156 3.609375 l +9.550781 1.417969 l +9.761719 0.550781 10.453125 0.000000 11.355469 0.000000 c +h +11.542969 1.828125 m +11.343750 1.828125 11.238281 1.910156 11.203125 2.097656 c +10.500000 5.003906 l +9.785156 5.179688 9.117188 5.460938 8.613281 5.777344 c +6.058594 4.207031 l +5.917969 4.101562 5.765625 4.125000 5.625000 4.265625 c +4.242188 5.648438 l +4.113281 5.777344 4.101562 5.917969 4.195312 6.082031 c +5.765625 8.613281 l +5.496094 9.105469 5.191406 9.773438 5.003906 10.488281 c +2.097656 11.179688 l +1.910156 11.214844 1.828125 11.320312 1.828125 11.519531 c +1.828125 13.476562 l +1.828125 13.687500 1.898438 13.781250 2.097656 13.816406 c +4.992188 14.519531 l +5.179688 15.281250 5.531250 15.972656 5.742188 16.406250 c +4.183594 18.937500 l +4.078125 19.113281 4.089844 19.253906 4.218750 19.394531 c +5.613281 20.753906 l +5.753906 20.894531 5.882812 20.917969 6.058594 20.812500 c +8.589844 19.277344 l +9.093750 19.558594 9.808594 19.851562 10.511719 20.050781 c +11.203125 22.957031 l +11.238281 23.144531 11.343750 23.226562 11.542969 23.226562 c +13.535156 23.226562 l +13.734375 23.226562 13.839844 23.144531 13.863281 22.957031 c +14.578125 20.027344 l +15.304688 19.839844 15.937500 19.546875 16.464844 19.265625 c +19.007812 20.812500 l +19.195312 20.917969 19.312500 20.894531 19.464844 20.753906 c +20.847656 19.394531 l +20.988281 19.253906 20.988281 19.113281 20.882812 18.937500 c +19.324219 16.406250 l +19.546875 15.972656 19.886719 15.281250 20.074219 14.519531 c +22.980469 13.816406 l +23.167969 13.781250 23.250000 13.687500 23.250000 13.476562 c +23.250000 11.519531 l +23.250000 11.320312 23.156250 11.214844 22.980469 11.179688 c +20.062500 10.488281 l +19.875000 9.773438 19.582031 9.105469 19.300781 8.613281 c +20.871094 6.082031 l +20.964844 5.917969 20.964844 5.777344 20.824219 5.648438 c +19.453125 4.265625 l +19.300781 4.125000 19.160156 4.101562 19.007812 4.207031 c +16.453125 5.777344 l +15.949219 5.460938 15.292969 5.179688 14.578125 5.003906 c +13.863281 2.097656 l +13.839844 1.910156 13.734375 1.828125 13.535156 1.828125 c +11.542969 1.828125 l +h +12.539062 8.050781 m +14.988281 8.050781 17.003906 10.066406 17.003906 12.527344 c +17.003906 14.964844 14.988281 16.980469 12.539062 16.980469 c +10.089844 16.980469 8.062500 14.964844 8.062500 12.527344 c +8.062500 10.078125 10.078125 8.050781 12.539062 8.050781 c +h +12.539062 9.867188 m +11.085938 9.867188 9.890625 11.062500 9.890625 12.527344 c +9.890625 13.968750 11.085938 15.164062 12.539062 15.164062 c +13.968750 15.164062 15.164062 13.968750 15.164062 12.527344 c +15.164062 11.074219 13.968750 9.867188 12.539062 9.867188 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 4265 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000004355 00000 n +0000004378 00000 n +0000004551 00000 n +0000004625 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +4684 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputStickersIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputStickersIcon.imageset/Contents.json new file mode 100644 index 0000000000..0f561e6972 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputStickersIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "ic_stickers.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputStickersIcon.imageset/ic_stickers.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputStickersIcon.imageset/ic_stickers.pdf new file mode 100644 index 0000000000..24ac31eb23 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Chat/Input/Media/EntityInputStickersIcon.imageset/ic_stickers.pdf @@ -0,0 +1,431 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.000000 2.336044 cm +0.000000 0.000000 0.000000 scn +12.629680 1.768461 m +12.783916 0.952917 l +12.629680 1.768461 l +h +16.593809 4.257765 m +17.180708 3.670866 l +16.593809 4.257765 l +h +14.895066 2.706814 m +14.427451 3.392551 l +14.895066 2.706814 l +h +21.895494 11.034276 m +22.711039 10.880039 l +21.895494 11.034276 l +h +19.406189 7.070145 m +18.819290 7.657043 l +19.406189 7.070145 l +h +20.957142 8.768889 m +20.271404 9.236505 l +20.957142 8.768889 l +h +0.483443 6.107693 m +-0.289235 5.804592 l +0.483443 6.107693 l +h +4.443737 2.147398 m +4.140636 1.374722 l +4.443737 2.147398 l +h +21.516558 19.220219 m +22.289234 19.523319 l +21.516558 19.220219 l +h +3.962814 22.970737 m +3.602690 23.718542 l +3.962814 22.970737 l +h +0.693218 19.701141 m +-0.054586 20.061266 l +0.693218 19.701141 l +h +11.000000 22.833956 m +11.668203 22.833956 l +11.668203 24.493956 l +11.000000 24.493956 l +11.000000 22.833956 l +h +0.830000 11.995752 m +0.830000 12.663956 l +-0.830000 12.663956 l +-0.830000 11.995752 l +0.830000 11.995752 l +h +18.819290 7.657043 m +16.006910 4.844664 l +17.180708 3.670866 l +19.993088 6.483246 l +18.819290 7.657043 l +h +10.331797 0.833956 m +11.464174 0.833956 12.137112 0.830593 12.783916 0.952917 c +12.475444 2.584003 l +12.017082 2.497318 11.526485 2.493956 10.331797 2.493956 c +10.331797 0.833956 l +h +16.006910 4.844664 m +15.162139 3.999891 14.812858 3.655367 14.427451 3.392551 c +15.362681 2.021076 l +15.906537 2.391941 16.379997 2.870155 17.180708 3.670866 c +16.006910 4.844664 l +h +12.783916 0.952917 m +13.707870 1.127657 14.585788 1.491302 15.362681 2.021076 c +14.427451 3.392551 l +13.839379 2.991537 13.174834 2.716274 12.475444 2.584003 c +12.783916 0.952917 l +h +21.170000 13.332159 m +21.170000 12.137469 21.166637 11.646873 21.079952 11.188512 c +22.711039 10.880039 l +22.833363 11.526844 22.830000 12.199781 22.830000 13.332159 c +21.170000 13.332159 l +h +19.993088 6.483246 m +20.793798 7.283958 21.272015 7.757419 21.642879 8.301274 c +20.271404 9.236505 l +20.008589 8.851098 19.664062 8.501816 18.819290 7.657043 c +19.993088 6.483246 l +h +21.079952 11.188512 m +20.947681 10.489121 20.672419 9.824576 20.271404 9.236505 c +21.642879 8.301274 l +22.172653 9.078168 22.536299 9.956086 22.711039 10.880039 c +21.079952 11.188512 l +h +-0.830000 11.995752 m +-0.830000 10.454670 -0.830408 9.259295 -0.767823 8.296148 c +-0.704785 7.326029 -0.575355 6.533985 -0.289235 5.804592 c +1.256120 6.410791 l +1.058798 6.913816 0.946507 7.513920 0.888684 8.403788 c +0.830408 9.300627 0.830000 10.433072 0.830000 11.995752 c +-0.830000 11.995752 l +h +10.331797 2.493956 m +8.769116 2.493956 7.636671 2.494364 6.739831 2.552639 c +5.849965 2.610462 5.249860 2.722754 4.746836 2.920076 c +4.140636 1.374722 l +4.870029 1.088600 5.662074 0.959171 6.632193 0.896133 c +7.595339 0.833548 8.790714 0.833956 10.331797 0.833956 c +10.331797 2.493956 l +h +-0.289235 5.804592 m +0.506586 3.775846 2.111891 2.170542 4.140636 1.374722 c +4.746836 2.920076 l +3.148195 3.547178 1.883223 4.812151 1.256120 6.410791 c +-0.289235 5.804592 l +h +11.668203 22.833956 m +13.230883 22.833956 14.363329 22.833548 15.260168 22.775272 c +16.150036 22.717449 16.750139 22.605158 17.253164 22.407835 c +17.859364 23.953190 l +17.129971 24.239311 16.337927 24.368740 15.367807 24.431778 c +14.404661 24.494364 13.209285 24.493956 11.668203 24.493956 c +11.668203 22.833956 l +h +22.830000 13.332159 m +22.830000 14.873241 22.830408 16.068617 22.767822 17.031763 c +22.704784 18.001881 22.575356 18.793926 22.289234 19.523319 c +20.743879 18.917120 l +20.941202 18.414095 21.053493 17.813992 21.111317 16.924124 c +21.169592 16.027285 21.170000 14.894838 21.170000 13.332159 c +22.830000 13.332159 l +h +17.253164 22.407835 m +18.851805 21.780733 20.116777 20.515760 20.743879 18.917120 c +22.289234 19.523319 l +21.493414 21.552065 19.888109 23.157370 17.859364 23.953190 c +17.253164 22.407835 l +h +11.000000 24.493956 m +9.147122 24.493956 7.709934 24.494541 6.557355 24.404799 c +5.395766 24.314354 4.453906 24.128466 3.602690 23.718542 c +4.322937 22.222933 l +4.911203 22.506227 5.622035 22.666948 6.686217 22.749807 c +7.759411 22.833370 9.121075 22.833956 11.000000 22.833956 c +11.000000 24.493956 l +h +0.830000 12.663956 m +0.830000 14.542881 0.830586 15.904545 0.914148 16.977737 c +0.997008 18.041920 1.157728 18.752752 1.441022 19.341019 c +-0.054586 20.061266 l +-0.464510 19.210049 -0.650399 18.268190 -0.740843 17.106600 c +-0.830586 15.954021 -0.830000 14.516833 -0.830000 12.663956 c +0.830000 12.663956 l +h +3.602690 23.718542 m +2.004527 22.948908 0.715049 21.659428 -0.054586 20.061266 c +1.441022 19.341019 l +2.047490 20.600363 3.063593 21.616465 4.322937 22.222933 c +3.602690 23.718542 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 14.000000 3.169342 cm +0.000000 0.000000 0.000000 scn +2.895337 1.513170 m +2.155802 1.889982 l +2.895337 1.513170 l +h +2.349067 0.966900 m +2.725879 0.227365 l +2.349067 0.966900 l +h +4.396603 9.526595 m +4.019791 10.266130 l +4.396603 9.526595 l +h +3.304063 8.434055 m +2.564527 8.810867 l +3.304063 8.434055 l +h +11.863758 10.481591 m +11.124223 10.858403 l +11.863758 10.481591 l +h +11.317488 9.935321 m +11.694300 9.195786 l +11.317488 9.935321 l +h +11.170000 12.830658 m +11.170000 11.799080 l +12.830000 11.799080 l +12.830000 12.830658 l +11.170000 12.830658 l +h +10.000000 10.629079 m +7.031581 10.629079 l +7.031581 8.969079 l +10.000000 8.969079 l +10.000000 10.629079 l +h +2.201579 5.799079 m +2.201579 2.830659 l +3.861579 2.830659 l +3.861579 5.799079 l +2.201579 5.799079 l +h +1.031579 1.660658 m +0.000000 1.660658 l +0.000000 0.000658 l +1.031579 0.000658 l +1.031579 1.660658 l +h +2.201579 2.830659 m +2.201579 2.466930 2.200933 2.244051 2.187305 2.077250 c +2.180954 1.999516 2.172969 1.952377 2.166202 1.923643 c +2.162982 1.909968 2.160270 1.901502 2.158613 1.896861 c +2.157021 1.892399 2.156040 1.890450 2.155802 1.889982 c +3.634872 1.136358 l +3.772548 1.406561 3.820568 1.682304 3.841792 1.942073 c +3.862224 2.192151 3.861579 2.494322 3.861579 2.830659 c +2.201579 2.830659 l +h +1.031579 0.000658 m +1.367916 0.000658 1.670086 0.000012 1.920165 0.020445 c +2.179933 0.041669 2.455676 0.089689 2.725879 0.227365 c +1.972255 1.706435 l +1.971787 1.706197 1.969838 1.705216 1.965376 1.703624 c +1.960735 1.701967 1.952269 1.699255 1.938594 1.696035 c +1.909860 1.689268 1.862720 1.681283 1.784988 1.674932 c +1.618186 1.661304 1.395307 1.660658 1.031579 1.660658 c +1.031579 0.000658 l +h +2.155802 1.889982 m +2.115535 1.810954 2.051283 1.746702 1.972255 1.706435 c +2.725879 0.227365 l +3.117256 0.426782 3.435456 0.744981 3.634872 1.136358 c +2.155802 1.889982 l +h +7.031581 10.629079 m +6.345211 10.629079 5.780515 10.629725 5.321996 10.592262 c +4.853787 10.554008 4.423688 10.471928 4.019791 10.266130 c +4.773415 8.787060 l +4.904296 8.853747 5.091620 8.907908 5.457173 8.937775 c +5.832415 8.968433 6.317819 8.969079 7.031581 8.969079 c +7.031581 10.629079 l +h +3.861579 5.799079 m +3.861579 6.512841 3.862224 6.998244 3.892883 7.373486 c +3.922750 7.739038 3.976911 7.926362 4.043598 8.057243 c +2.564527 8.810867 l +2.358731 8.406969 2.276650 7.976871 2.238396 7.508663 c +2.200933 7.050144 2.201579 6.485449 2.201579 5.799079 c +3.861579 5.799079 l +h +4.019791 10.266130 m +3.393211 9.946873 2.883785 9.437447 2.564527 8.810867 c +4.043598 8.057243 l +4.203707 8.371473 4.459184 8.626951 4.773415 8.787060 c +4.019791 10.266130 l +h +11.170000 11.799080 m +11.170000 11.435350 11.169354 11.212472 11.155726 11.045670 c +11.149375 10.967937 11.141390 10.920798 11.134623 10.892064 c +11.131403 10.878389 11.128691 10.869923 11.127034 10.865282 c +11.125442 10.860820 11.124461 10.858871 11.124223 10.858403 c +12.603293 10.104778 l +12.740969 10.374982 12.788989 10.650725 12.810213 10.910493 c +12.830646 11.160572 12.830000 11.462742 12.830000 11.799080 c +11.170000 11.799080 l +h +10.000000 8.969079 m +10.336337 8.969079 10.638507 8.968433 10.888586 8.988866 c +11.148354 9.010090 11.424097 9.058110 11.694300 9.195786 c +10.940676 10.674856 l +10.940208 10.674618 10.938259 10.673637 10.933797 10.672045 c +10.929156 10.670388 10.920690 10.667677 10.907015 10.664455 c +10.878281 10.657689 10.831141 10.649704 10.753409 10.643353 c +10.586607 10.629725 10.363729 10.629079 10.000000 10.629079 c +10.000000 8.969079 l +h +11.124223 10.858403 m +11.083956 10.779375 11.019704 10.715123 10.940676 10.674856 c +11.694300 9.195786 l +12.085677 9.395203 12.403876 9.713402 12.603293 10.104778 c +11.124223 10.858403 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 10.000000 16.250000 cm +0.000000 0.000000 0.000000 scn +3.000000 1.750000 m +3.000000 0.783502 2.328427 0.000000 1.500000 0.000000 c +0.671573 0.000000 0.000000 0.783502 0.000000 1.750000 c +0.000000 2.716498 0.671573 3.500000 1.500000 3.500000 c +2.328427 3.500000 3.000000 2.716498 3.000000 1.750000 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 17.000000 16.250000 cm +0.000000 0.000000 0.000000 scn +3.000000 1.750000 m +3.000000 0.783502 2.328427 0.000000 1.500000 0.000000 c +0.671573 0.000000 0.000000 0.783502 0.000000 1.750000 c +0.000000 2.716498 0.671573 3.500000 1.500000 3.500000 c +2.328427 3.500000 3.000000 2.716498 3.000000 1.750000 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 11.000000 8.230942 cm +0.000000 0.000000 0.000000 scn +0.664000 4.267058 m +0.388962 4.633775 -0.131283 4.708096 -0.498000 4.433058 c +-0.864717 4.158020 -0.939038 3.637775 -0.664000 3.271058 c +0.664000 4.267058 l +h +4.000000 1.769054 m +3.999999 0.939054 l +4.000000 0.939054 l +4.000000 1.769054 l +h +6.332036 1.389412 m +6.752154 1.572791 6.944070 2.062022 6.760692 2.482140 c +6.577314 2.902259 6.088083 3.094175 5.667964 2.910796 c +6.332036 1.389412 l +h +0.000000 3.769058 m +-0.664000 3.271058 -0.663799 3.270791 -0.663593 3.270516 c +-0.663516 3.270414 -0.663304 3.270131 -0.663150 3.269927 c +-0.662843 3.269519 -0.662512 3.269080 -0.662157 3.268610 c +-0.661448 3.267670 -0.660643 3.266607 -0.659743 3.265423 c +-0.657943 3.263054 -0.655763 3.260200 -0.653203 3.256877 c +-0.648084 3.250230 -0.641445 3.241700 -0.633302 3.231414 c +-0.617020 3.210848 -0.594689 3.183217 -0.566423 3.149538 c +-0.509945 3.082245 -0.429440 2.990409 -0.325813 2.882277 c +-0.119243 2.666726 0.183137 2.382302 0.574317 2.097807 c +1.353481 1.531142 2.521308 0.939057 3.999999 0.939054 c +4.000001 2.599054 l +2.978692 2.599056 2.146519 3.006973 1.550683 3.440309 c +1.254363 3.655814 1.025493 3.871391 0.872688 4.030839 c +0.796627 4.110207 0.740414 4.174622 0.705095 4.216704 c +0.687462 4.237712 0.675126 4.253050 0.668214 4.261781 c +0.664761 4.266143 0.662671 4.268843 0.661961 4.269765 c +0.661607 4.270226 0.661598 4.270240 0.661937 4.269794 c +0.662106 4.269571 0.662363 4.269233 0.662706 4.268778 c +0.662878 4.268550 0.663072 4.268293 0.663288 4.268007 c +0.663395 4.267863 0.663574 4.267626 0.663627 4.267555 c +0.663811 4.267310 0.664000 4.267058 0.000000 3.769058 c +h +4.000000 0.939054 m +4.844985 0.939054 5.627935 1.082078 6.332036 1.389412 c +5.667964 2.910796 l +5.208086 2.710063 4.657703 2.599054 4.000000 2.599054 c +4.000000 0.939054 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 10900 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000010990 00000 n +0000011014 00000 n +0000011187 00000 n +0000011261 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +11320 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 5fd0ef4b1a..9ff570f2e4 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -917,8 +917,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G transitionCompletion() }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -1494,13 +1494,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G attributes.append(TextEntitiesMessageAttribute(entities: entities)) } strongSelf.sendMessages([.message(text: text, attributes: attributes, mediaReference: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil, correlationId: nil)]) - }, sendSticker: { [weak self] fileReference, silentPosting, schedule, query, clearInput, sourceNode, sourceRect in + }, sendSticker: { [weak self] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect in guard let strongSelf = self else { return false } if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceView, sourceRect) return false } @@ -1517,7 +1517,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } var shouldAnimateMessageTransition = strongSelf.chatDisplayNode.shouldAnimateMessageTransition - if sourceNode is ChatEmptyNodeStickerContentNode { + if let _ = sourceView.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode { shouldAnimateMessageTransition = true } @@ -1545,7 +1545,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, shouldAnimateMessageTransition ? correlationId : nil) if shouldAnimateMessageTransition { - if let sourceNode = sourceNode as? ChatMediaInputStickerGridItemNode { + if let sourceNode = sourceView.asyncdisplaykit_node as? ChatMediaInputStickerGridItemNode { strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: { guard let strongSelf = self else { return @@ -1562,12 +1562,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return current }) }) - } else if let sourceNode = sourceNode as? HorizontalStickerGridItemNode { + } else if let sourceNode = sourceView.asyncdisplaykit_node as? HorizontalStickerGridItemNode { strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .mediaPanel(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) - } else if let sourceNode = sourceNode as? StickerPaneSearchStickerItemNode { + } else if let sourceNode = sourceView.asyncdisplaykit_node as? StickerPaneSearchStickerItemNode { strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .inputPanelSearch(itemNode: sourceNode), replyPanel: replyPanel), initiated: {}) - } else if let sourceNode = sourceNode as? ChatEmptyNodeStickerContentNode { + } else if let sourceNode = sourceView.asyncdisplaykit_node as? ChatEmptyNodeStickerContentNode { strongSelf.chatDisplayNode.messageTransitionNode.add(correlationId: correlationId, source: .stickerMediaInput(input: .emptyPanel(itemNode: sourceNode), replyPanel: nil), initiated: {}) + } else { + } } @@ -1590,7 +1592,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }, sendGif: { [weak self] fileReference, sourceNode, sourceRect, silentPosting, schedule in if let strongSelf = self { if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode.view, sourceRect) return false } @@ -1632,7 +1634,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode, sourceRect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(sourceNode.view, sourceRect) return false } @@ -2947,7 +2949,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let strongSelf = self { if let _ = strongSelf.presentationInterfaceState.slowmodeState { if let rect = strongSelf.chatDisplayNode.frameForInputActionButton() { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode, rect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode.view, rect) } return } else { @@ -3288,8 +3290,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] { legacyMediaEditor(context: strongSelf.context, peer: peer, media: mediaReference, initialCaption: NSAttributedString(string: message.text), snapshots: [], transitionCompletion: nil, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -6128,7 +6130,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if strongSelf.presentationInterfaceState.interfaceState.editMessage == nil, let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { if let rect = strongSelf.chatDisplayNode.frameForAttachmentButton() { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode, rect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(strongSelf.chatDisplayNode.view, rect) } return } @@ -7197,7 +7199,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return false } if let _ = strongSelf.presentationInterfaceState.slowmodeState, strongSelf.presentationInterfaceState.subject != .scheduledMessages { - strongSelf.interfaceInteraction?.displaySlowmodeTooltip(node, rect) + strongSelf.interfaceInteraction?.displaySlowmodeTooltip(node.view, rect) return false } @@ -7581,9 +7583,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } } - }, sendSticker: { [weak self] file, clearInput, sourceNode, sourceRect in + }, sendSticker: { [weak self] file, clearInput, sourceView, sourceRect in if let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) { - return strongSelf.controllerInteraction?.sendSticker(file, false, false, nil, clearInput, sourceNode, sourceRect) ?? false + return strongSelf.controllerInteraction?.sendSticker(file, false, false, nil, clearInput, sourceView, sourceRect) ?? false } else { return false } @@ -8240,11 +8242,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) })] strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.ReportGroupLocation_Title, text: strongSelf.presentationData.strings.ReportGroupLocation_Text, actions: actions), in: .window(.root)) - }, displaySlowmodeTooltip: { [weak self] node, nodeRect in + }, displaySlowmodeTooltip: { [weak self] sourceView, nodeRect in guard let strongSelf = self, let slowmodeState = strongSelf.presentationInterfaceState.slowmodeState else { return } - let rect = node.view.convert(nodeRect, to: strongSelf.view) + let rect = sourceView.convert(nodeRect, to: strongSelf.view) if let slowmodeTooltipController = strongSelf.slowmodeTooltipController { if let arguments = slowmodeTooltipController.presentationArguments as? TooltipControllerPresentationArguments, case let .node(f) = arguments.sourceAndRect, let (previousNode, previousRect) = f() { if previousNode === strongSelf.chatDisplayNode && previousRect == rect { @@ -8559,6 +8561,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return $0.updatedShowWebView(f($0.showWebView)) }) } + }, insertText: { [weak self] text in + guard let strongSelf = self else { + return + } + if !strongSelf.chatDisplayNode.isTextInputPanelActive { + return + } + guard let textInputPanelNode = strongSelf.chatDisplayNode.textInputPanelNode else { + return + } + textInputPanelNode.insertText(string: text) + }, backwardsDeleteText: { [weak self] in + guard let strongSelf = self else { + return + } + if !strongSelf.chatDisplayNode.isTextInputPanelActive { + return + } + guard let textInputPanelNode = strongSelf.chatDisplayNode.textInputPanelNode else { + return + } + textInputPanelNode.backwardsDeleteText() }, chatController: { [weak self] in return self }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get(), startingBot: self.startingBot.get(), unblockingPeer: self.unblockingPeer.get(), searching: self.searching.get(), loadingMessage: self.loadingMessage.get(), inlineSearch: self.performingInlineSearch.get())) @@ -10759,8 +10783,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -11364,8 +11388,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -11462,8 +11486,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -11672,8 +11696,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } controller.presentStickers = { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -11769,8 +11793,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) controller.presentStickers = { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -11816,8 +11840,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -11873,8 +11897,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) controller.presentStickers = { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -12723,8 +12747,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G self?.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil) }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.present(controller, in: .window(.root)) @@ -12944,8 +12968,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G strongSelf.sendMessages([updatedMessage]) } - }, displaySlowmodeTooltip: { [weak self] node, rect in - self?.interfaceInteraction?.displaySlowmodeTooltip(node, rect) + }, displaySlowmodeTooltip: { [weak self] view, rect in + self?.interfaceInteraction?.displaySlowmodeTooltip(view, rect) }, presentSchedulePicker: { [weak self] done in if let strongSelf = self { strongSelf.presentScheduleTimePicker(completion: { [weak self] time in @@ -13131,7 +13155,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let _ = self.presentationInterfaceState.slowmodeState, !isScheduledMessages { if let rect = self.chatDisplayNode.frameForInputActionButton() { - self.interfaceInteraction?.displaySlowmodeTooltip(self.chatDisplayNode, rect) + self.interfaceInteraction?.displaySlowmodeTooltip(self.chatDisplayNode.view, rect) } return } @@ -14821,8 +14845,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G default: break } - }, sendFile: nil, sendSticker: { [weak self] f, sourceNode, sourceRect in - return self?.interfaceInteraction?.sendSticker(f, true, sourceNode, sourceRect) ?? false + }, sendFile: nil, sendSticker: { [weak self] f, sourceView, sourceRect in + return self?.interfaceInteraction?.sendSticker(f, true, sourceView, sourceRect) ?? false }, requestMessageActionUrlAuth: { [weak self] subject in if case let .url(url) = subject { self?.controllerInteraction?.requestMessageActionUrlAuth(url, subject) diff --git a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift index d950b8235e..1fac0dc958 100644 --- a/submodules/TelegramUI/Sources/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/Sources/ChatControllerInteraction.swift @@ -73,7 +73,7 @@ public final class ChatControllerInteraction { let toggleMessagesSelection: ([MessageId], Bool) -> Void let sendCurrentMessage: (Bool) -> Void let sendMessage: (String) -> Void - let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, ASDisplayNode, CGRect) -> Bool + let sendSticker: (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect) -> Bool let sendGif: (FileMediaReference, ASDisplayNode, CGRect, Bool, Bool) -> Bool let sendBotContextResultAsGif: (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect, Bool) -> Bool let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void @@ -176,7 +176,7 @@ public final class ChatControllerInteraction { toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, - sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, ASDisplayNode, CGRect) -> Bool, + sendSticker: @escaping (FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect, Bool, Bool) -> Bool, sendBotContextResultAsGif: @escaping (ChatContextResultCollection, ChatContextResult, ASDisplayNode, CGRect, Bool) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool, Bool) -> Void, diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 4b963f0286..81c407e371 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -83,7 +83,6 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let backgroundNode: WallpaperBackgroundNode let historyNode: ChatHistoryListNode - //let historyScrollingArea: SparseDiscreteScrollingArea var blurredHistoryNode: ASImageNode? let historyNodeContainer: ASDisplayNode let loadingNode: ChatLoadingNode @@ -100,7 +99,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let inputPanelContainerNode: SparseNode private let inputPanelBackgroundNode: NavigationBackgroundNode + private var intrinsicInputPanelBackgroundNodeSize: CGSize? private let inputPanelBackgroundSeparatorNode: ASDisplayNode + private let inputPanelBottomBackgroundSeparatorNode: ASDisplayNode private var plainInputSeparatorAlpha: CGFloat? private var usePlainInputSeparator: Bool @@ -122,7 +123,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var disappearingNode: ChatInputNode? private(set) var textInputPanelNode: ChatTextInputPanelNode? + private var inputMediaNode: ChatMediaInputNode? + private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData? + private var inputMediaNodeDataPromise = Promise() + private var didInitializeInputMediaNodeDataPromise: Bool = false + private var inputMediaNodeDataDisposable: Disposable? let navigateButtons: ChatHistoryNavigationButtons @@ -378,6 +384,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelSeparatorColor self.inputPanelBackgroundSeparatorNode.isLayerBacked = true + self.inputPanelBottomBackgroundSeparatorNode = ASDisplayNode() + self.inputPanelBottomBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputMediaPanel.panelSeparatorColor + self.inputPanelBottomBackgroundSeparatorNode.isLayerBacked = true + self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme, dateTimeFormat: self.chatPresentationInterfaceState.dateTimeFormat) self.navigateButtons.accessibilityElementsHidden = true @@ -499,6 +509,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.addSubnode(self.inputPanelContainerNode) self.inputPanelContainerNode.addSubnode(self.inputPanelBackgroundNode) self.inputPanelContainerNode.addSubnode(self.inputPanelBackgroundSeparatorNode) + self.inputPanelContainerNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode) self.addSubnode(self.inputContextPanelContainer) @@ -553,12 +564,21 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.textInputPanelNode?.updateActivity = { [weak self] in self?.updateTypingActivity(true) } + + self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get() + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.inputMediaNodeData = value + }) } deinit { self.interactiveEmojisDisposable?.dispose() self.openStickersDisposable?.dispose() self.displayVideoUnmuteTipDisposable?.dispose() + self.inputMediaNodeDataDisposable?.dispose() } override func didLoad() { @@ -886,14 +906,17 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let maximumInputNodeHeight = layout.size.height - max(navigationBarHeight + (titleAccessoryPanelBackgroundHeight ?? 0.0), layout.safeInsets.top) - inputPanelNodeBaseHeight var dismissedInputNode: ChatInputNode? + var dismissedInputNodeExternalTopPanelContainer: UIView? var immediatelyLayoutInputNodeAndAnimateAppearance = false var inputNodeHeightAndOverflow: (CGFloat, CGFloat)? - if let inputNode = inputNodeForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentNode: self.inputNode, interfaceInteraction: self.interfaceInteraction, inputMediaNode: self.inputMediaNode, controllerInteraction: self.controllerInteraction, inputPanelNode: self.inputPanelNode) { - if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { + if let inputNode = inputNodeForChatPresentationIntefaceState(self.chatPresentationInterfaceState, context: self.context, currentNode: self.inputNode, interfaceInteraction: self.interfaceInteraction, inputMediaNode: self.inputMediaNode, controllerInteraction: self.controllerInteraction, inputPanelNode: self.inputPanelNode, makeMediaInputNode: { + return self.makeMediaInputNode() + }) { + /*if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { if inputPanelNode.isFocused { self.context.sharedContext.mainWindow?.simulateKeyboardDismiss(transition: .animated(duration: 0.5, curve: .spring)) } - } + }*/ if let inputMediaNode = inputNode as? ChatMediaInputNode, self.inputMediaNode == nil { self.inputMediaNode = inputMediaNode inputMediaNode.requestDisableStickerAnimations = { [weak self] disabled in @@ -901,20 +924,34 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } if self.inputNode != inputNode { + inputNode.topBackgroundExtensionUpdated = { [weak self] transition in + self?.updateInputPanelBackgroundExtension(transition: transition) + } + dismissedInputNode = self.inputNode + dismissedInputNodeExternalTopPanelContainer = self.inputNode?.externalTopPanelContainer self.inputNode = inputNode inputNode.alpha = 1.0 inputNode.layer.removeAnimation(forKey: "opacity") immediatelyLayoutInputNodeAndAnimateAppearance = true - if let inputPanelNode = self.inputPanelNode, inputPanelNode.supernode != nil { - self.inputPanelContainerNode.insertSubnode(inputNode, aboveSubnode: inputPanelNode) - } else { - self.inputPanelContainerNode.insertSubnode(inputNode, aboveSubnode: self.inputPanelBackgroundNode) + /*if let inputPanelNode = self.inputPanelNode, inputPanelNode.supernode != nil { + self.inputPanelContainerNode.insertSubnode(inputNode, belowSubnode: inputPanelNode) + } else {*/ + self.inputPanelContainerNode.insertSubnode(inputNode, belowSubnode: self.inputPanelBackgroundNode) + //} + + if let externalTopPanelContainer = inputNode.externalTopPanelContainer { + if let inputPanelNode = self.inputPanelNode, inputPanelNode.supernode != nil { + self.inputPanelContainerNode.view.insertSubview(externalTopPanelContainer, belowSubview: inputPanelNode.view) + } else { + self.inputPanelContainerNode.view.addSubview(externalTopPanelContainer) + } } } inputNodeHeightAndOverflow = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelNodeBaseHeight, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: layout.deviceMetrics, isVisible: self.isInFocus) } else if let inputNode = self.inputNode { dismissedInputNode = inputNode + dismissedInputNodeExternalTopPanelContainer = inputNode.externalTopPanelContainer self.inputNode = nil } @@ -1357,7 +1394,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let previousInputPanelBackgroundFrame = self.inputPanelBackgroundNode.frame transition.updateFrame(node: self.inputPanelContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame) - self.inputPanelBackgroundNode.update(size: CGSize(width: apparentInputBackgroundFrame.size.width, height: apparentInputBackgroundFrame.size.height + 41.0 + 31.0), transition: transition) + + let intrinsicInputPanelBackgroundNodeSize = CGSize(width: apparentInputBackgroundFrame.size.width, height: apparentInputBackgroundFrame.size.height) + self.intrinsicInputPanelBackgroundNodeSize = intrinsicInputPanelBackgroundNodeSize + var inputPanelBackgroundExtension: CGFloat = 0.0 + if let inputNode = self.inputNode { + inputPanelBackgroundExtension = inputNode.topBackgroundExtension + } + self.inputPanelBackgroundNode.update(size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: intrinsicInputPanelBackgroundNodeSize.height + inputPanelBackgroundExtension), transition: transition) + transition.updateFrame(node: self.inputPanelBottomBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.inputPanelBackgroundNode.frame.maxY + inputPanelBackgroundExtension), size: CGSize(width: self.inputPanelBackgroundNode.bounds.width, height: UIScreenPixel))) + transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: CGSize(width: apparentInputBackgroundFrame.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.navigateButtons, frame: apparentNavigateButtonsFrame) @@ -1437,8 +1483,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { adjustedForPreviousInputHeightFrame.origin.y += heightDifference inputNode.frame = adjustedForPreviousInputHeightFrame transition.updateFrame(node: inputNode, frame: inputNodeFrame) + + if let externalTopPanelContainer = inputNode.externalTopPanelContainer { + externalTopPanelContainer.frame = CGRect(origin: adjustedForPreviousInputHeightFrame.origin, size: CGSize(width: adjustedForPreviousInputHeightFrame.width, height: 0.0)) + transition.updateFrame(view: externalTopPanelContainer, frame: CGRect(origin: inputNodeFrame.origin, size: CGSize(width: inputNodeFrame.width, height: 0.0))) + } } else { transition.updateFrame(node: inputNode, frame: inputNodeFrame) + if let externalTopPanelContainer = inputNode.externalTopPanelContainer { + transition.updateFrame(view: externalTopPanelContainer, frame: CGRect(origin: inputNodeFrame.origin, size: CGSize(width: inputNodeFrame.width, height: 0.0))) + } } } @@ -1628,6 +1682,24 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } else { targetY = layout.size.height } + + if let dismissedInputNodeExternalTopPanelContainer = dismissedInputNodeExternalTopPanelContainer { + transition.updateFrame(view: dismissedInputNodeExternalTopPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: targetY), size: CGSize(width: layout.size.width, height: 0.0)), force: true, completion: { [weak self, weak dismissedInputNodeExternalTopPanelContainer] completed in + if let strongSelf = self, let dismissedInputNodeExternalTopPanelContainer = dismissedInputNodeExternalTopPanelContainer { + if strongSelf.inputNode?.externalTopPanelContainer !== dismissedInputNodeExternalTopPanelContainer { + dismissedInputNodeExternalTopPanelContainer.alpha = 0.0 + dismissedInputNodeExternalTopPanelContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak dismissedInputNodeExternalTopPanelContainer] completed in + if completed, let strongSelf = self, let dismissedInputNodeExternalTopPanelContainer = dismissedInputNodeExternalTopPanelContainer { + if strongSelf.inputNode?.externalTopPanelContainer !== dismissedInputNodeExternalTopPanelContainer { + dismissedInputNodeExternalTopPanelContainer.removeFromSuperview() + } + } + }) + } + } + }) + } + transition.updateFrame(node: dismissedInputNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetY), size: CGSize(width: layout.size.width, height: max(insets.bottom, dismissedInputNode.bounds.size.height))), force: true, completion: { [weak self, weak dismissedInputNode] completed in if let dismissedInputNode = dismissedInputNode { if let strongSelf = self { @@ -1688,6 +1760,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { //self.notifyTransitionCompletionListeners(transition: transition) } + private func updateInputPanelBackgroundExtension(transition: ContainedViewLayoutTransition) { + guard let intrinsicInputPanelBackgroundNodeSize = self.intrinsicInputPanelBackgroundNodeSize else { + return + } + + var extensionValue: CGFloat = 0.0 + if let inputNode = self.inputNode { + extensionValue = inputNode.topBackgroundExtension + } + + self.inputPanelBackgroundNode.update(size: CGSize(width: intrinsicInputPanelBackgroundNodeSize.width, height: intrinsicInputPanelBackgroundNodeSize.height + extensionValue), transition: transition) + transition.updateFrame(node: self.inputPanelBottomBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.inputPanelBackgroundNode.frame.maxY + extensionValue), size: CGSize(width: self.inputPanelBackgroundNode.bounds.width, height: UIScreenPixel))) + } + private func notifyTransitionCompletionListeners(transition: ContainedViewLayoutTransition) { if !self.onLayoutCompletions.isEmpty { let onLayoutCompletions = self.onLayoutCompletions @@ -1700,14 +1786,28 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private func chatPresentationInterfaceStateRequiresInputFocus(_ state: ChatPresentationInterfaceState) -> Bool { switch state.inputMode { - case .text: - if state.interfaceState.selectionState != nil { - return false - } else { - return true - } - default: + case .text: + if state.interfaceState.selectionState != nil { return false + } else { + return true + } + case .media: + return true + default: + return false + } + } + + private let emptyInputView = UIView() + private func chatPresentationInterfaceStateInputView(_ state: ChatPresentationInterfaceState) -> UIView? { + switch state.inputMode { + case .text: + return nil + case .media: + return self.emptyInputView + default: + return nil } } @@ -1730,6 +1830,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) let updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) + let updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState self.chatPresentationInterfaceState = chatPresentationInterfaceState @@ -1746,6 +1847,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } self.updatePlainInputSeparator(transition: .immediate) self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelSeparatorColor + self.inputPanelBottomBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputMediaPanel.panelSeparatorColor self.backgroundNode.updateBubbleTheme(bubbleTheme: chatPresentationInterfaceState.theme, bubbleCorners: chatPresentationInterfaceState.bubbleCorners) } @@ -1840,6 +1942,16 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.navigationBar?.setContentNode(nil, animated: transitionIsAnimated) } + if let textView = self.textInputPanelNode?.textInputNode?.textView { + let updatedInputView = self.chatPresentationInterfaceStateInputView(chatPresentationInterfaceState) + if textView.inputView !== updatedInputView { + textView.inputView = updatedInputView + if textView.isFirstResponder { + textView.reloadInputViews() + } + } + } + if updatedInputFocus { if !self.ignoreUpdateHeight { self.scheduleLayoutTransitionRequest(layoutTransition) @@ -1963,8 +2075,30 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.setNeedsLayout() } + private func makeMediaInputNode() -> ChatInputNode? { + guard let inputMediaNodeData = self.inputMediaNodeData else { + return nil + } + + var peerId: PeerId? + if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation { + peerId = id + } + let _ = peerId + + let inputNode = ChatEntityKeyboardInputNode(context: self.context, currentInputData: inputMediaNodeData, updatedInputData: self.inputMediaNodeDataPromise.get()) + + return inputNode + } + func loadInputPanels(theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize) { - if self.inputMediaNode == nil { + if !self.didInitializeInputMediaNodeDataPromise, let interfaceInteraction = self.interfaceInteraction { + self.didInitializeInputMediaNodeDataPromise = true + + self.inputMediaNodeDataPromise.set(ChatEntityKeyboardInputNode.inputData(context: self.context, interfaceInteraction: interfaceInteraction)) + } + + if self.inputMediaNode == nil && !"".isEmpty { let peerId: PeerId? = self.chatPresentationInterfaceState.chatLocation.peerId let inputNode = ChatMediaInputNode(context: self.context, peerId: peerId, chatLocation: self.chatPresentationInterfaceState.chatLocation, controllerInteraction: self.controllerInteraction, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, theme: theme, strings: strings, fontSize: fontSize, gifPaneIsActiveUpdated: { [weak self] value in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { @@ -1989,9 +2123,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let (validLayout, _) = self.validLayout { let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, inputHeight: validLayout.inputHeight ?? 0.0, maximumHeight: validLayout.standardInputHeight, inputPanelHeight: 44.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, deviceMetrics: validLayout.deviceMetrics, isVisible: false) } - - self.textInputPanelNode?.loadTextInputNodeIfNeeded() } + + self.textInputPanelNode?.loadTextInputNodeIfNeeded() } func currentInputPanelFrame() -> CGRect? { @@ -2376,14 +2510,20 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } func openStickers() { - if let inputMediaNode = self.inputMediaNode, self.openStickersDisposable == nil { - self.openStickersDisposable = (inputMediaNode.ready - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] in - self?.openStickersDisposable = nil - self?.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in - return (.media(mode: .other, expanded: nil, focused: false), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + if let inputMediaNode = self.inputMediaNode { + if self.openStickersDisposable == nil { + self.openStickersDisposable = (inputMediaNode.ready + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] in + self?.openStickersDisposable = nil + self?.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.media(mode: .other, expanded: nil, focused: false), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + }) }) + } + } else { + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.media(mode: .other, expanded: nil, focused: false), state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) }) } } @@ -2409,7 +2549,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let _ = effectivePresentationInterfaceState.slowmodeState, !isScheduledMessages && scheduleTime == nil { if let rect = self.frameForInputActionButton() { - self.interfaceInteraction?.displaySlowmodeTooltip(self, rect) + self.interfaceInteraction?.displaySlowmodeTooltip(self.view, rect) } return } diff --git a/submodules/TelegramUI/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Sources/ChatEmptyNode.swift index 5b87ffd1fe..d14c8a99a1 100644 --- a/submodules/TelegramUI/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Sources/ChatEmptyNode.swift @@ -134,7 +134,7 @@ final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeSticke guard let stickerItem = self.stickerItem else { return } - let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self, self.stickerNode.bounds) + let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds) } func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { @@ -303,7 +303,7 @@ final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerC guard let stickerItem = self.stickerItem else { return } - let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self, self.stickerNode.bounds) + let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds) } func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift new file mode 100644 index 0000000000..3832a6764c --- /dev/null +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -0,0 +1,217 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import AccountContext +import ChatPresentationInterfaceState +import ComponentFlow +import EntityKeyboard +import AnimationCache +import MultiAnimationRenderer +import Postbox +import TelegramCore +import ComponentDisplayAdapters + +final class ChatEntityKeyboardInputNode: ChatInputNode { + struct InputData: Equatable { + let emoji: EmojiPagerContentComponent + let stickers: EmojiPagerContentComponent + + init( + emoji: EmojiPagerContentComponent, + stickers: EmojiPagerContentComponent + ) { + self.emoji = emoji + self.stickers = stickers + } + } + + static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction) -> Signal { + let emojiInputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { [weak interfaceInteraction] item, _, _ in + guard let interfaceInteraction = interfaceInteraction else { + return + } + interfaceInteraction.insertText(item.emoji) + }, + deleteBackwards: { [weak interfaceInteraction] in + guard let interfaceInteraction = interfaceInteraction else { + return + } + interfaceInteraction.backwardsDeleteText() + } + ) + let stickerInputInteraction = EmojiPagerContentComponent.InputInteraction( + performItemAction: { [weak interfaceInteraction] item, view, rect in + guard let interfaceInteraction = interfaceInteraction else { + return + } + let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, view, rect) + }, + deleteBackwards: { [weak interfaceInteraction] in + guard let interfaceInteraction = interfaceInteraction else { + return + } + interfaceInteraction.backwardsDeleteText() + } + ) + + let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { + return TempBox.shared.tempFile(fileName: "file").path + }) + let animationRenderer = MultiAnimationRendererImpl() + + let emojiItems: Signal = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) + |> map { animatedEmoji -> EmojiPagerContentComponent in + var emojiItems: [EmojiPagerContentComponent.Item] = [] + + switch animatedEmoji { + case let .result(_, items, _): + for item in items { + if let emoji = item.getStringRepresentationsOfIndexKeys().first { + let strippedEmoji = emoji.basicEmoji.0.strippedEmoji + emojiItems.append(EmojiPagerContentComponent.Item( + emoji: strippedEmoji, + file: item.file + )) + } + } + default: + break + } + + var itemGroups: [EmojiPagerContentComponent.ItemGroup] = [] + itemGroups.append(EmojiPagerContentComponent.ItemGroup( + id: "all", + title: nil, + items: emojiItems + )) + return EmojiPagerContentComponent( + context: context, + animationCache: animationCache, + animationRenderer: animationRenderer, + inputInteraction: emojiInputInteraction, + itemGroups: itemGroups, + itemLayoutType: .compact + ) + } + + let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers] + let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] + let stickerItems: Signal = context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000) + |> map { view -> EmojiPagerContentComponent in + struct ItemGroup { + var id: ItemCollectionId + var items: [EmojiPagerContentComponent.Item] + } + var itemGroups: [ItemGroup] = [] + var itemGroupIndexById: [AnyHashable: Int] = [:] + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + if !item.file.isAnimatedSticker { + continue + } + let resultItem = EmojiPagerContentComponent.Item( + emoji: "", + file: item.file + ) + 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])) + } + } + + return EmojiPagerContentComponent( + context: context, + animationCache: animationCache, + animationRenderer: animationRenderer, + inputInteraction: stickerInputInteraction, + itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in + var title: String? + for (id, info, _) in view.collectionInfos { + if id == group.id, let info = info as? StickerPackCollectionInfo { + title = info.title.uppercased() + break + } + } + + return EmojiPagerContentComponent.ItemGroup(id: group.id, title: title, items: group.items) + }, + itemLayoutType: .detailed + ) + } + + return combineLatest(queue: .mainQueue(), + emojiItems, + stickerItems + ) + |> map { emoji, stickers -> InputData in + return InputData( + emoji: emoji, + stickers: stickers + ) + } + } + + private let entityKeyboardView: ComponentHostView + + private var currentInputData: InputData + private var inputDataDisposable: Disposable? + + init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal) { + self.currentInputData = currentInputData + self.entityKeyboardView = ComponentHostView() + + super.init() + + self.view.addSubview(self.entityKeyboardView) + + self.externalTopPanelContainer = SparseContainerView() + + self.inputDataDisposable = (updatedInputData + |> deliverOnMainQueue).start(next: { [weak self] inputData in + guard let strongSelf = self else { + return + } + strongSelf.currentInputData = inputData + }) + } + + deinit { + self.inputDataDisposable?.dispose() + } + + override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) { + let entityKeyboardSize = self.entityKeyboardView.update( + transition: Transition(transition), + component: AnyComponent(EntityKeyboardComponent( + theme: interfaceState.theme, + bottomInset: bottomInset, + emojiContent: self.currentInputData.emoji, + stickerContent: self.currentInputData.stickers, + externalTopPanelContainer: self.externalTopPanelContainer, + topPanelExtensionUpdated: { [weak self] topPanelExtension, transition in + guard let strongSelf = self else { + return + } + if strongSelf.topBackgroundExtension != topPanelExtension { + strongSelf.topBackgroundExtension = topPanelExtension + strongSelf.topBackgroundExtensionUpdated?(transition.containedViewLayoutTransition) + } + } + )), + environment: {}, + containerSize: CGSize(width: width, height: standardInputHeight) + ) + transition.updateFrame(view: self.entityKeyboardView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: entityKeyboardSize)) + + return (standardInputHeight, 0.0) + } +} diff --git a/submodules/TelegramUI/Sources/ChatInputNode.swift b/submodules/TelegramUI/Sources/ChatInputNode.swift index 0b955aecc1..ca1ebffaab 100644 --- a/submodules/TelegramUI/Sources/ChatInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatInputNode.swift @@ -11,6 +11,11 @@ class ChatInputNode: ASDisplayNode { return .single(Void()) } + var externalTopPanelContainer: UIView? + + var topBackgroundExtension: CGFloat = 0.0 + var topBackgroundExtensionUpdated: ((ContainedViewLayoutTransition) -> Void)? + func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) { return (0.0, 0.0) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index 2e95f80d98..68d107097c 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -317,13 +317,9 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte if isTextEmpty && chatPresentationInterfaceState.hasBots && chatPresentationInterfaceState.hasBotCommands { accessoryItems.append(.commands) } - #if DEBUG + accessoryItems.append(.stickers(stickersEnabled)) - #else - if isTextEmpty { - accessoryItems.append(.stickers(stickersEnabled)) - } - #endif + if isTextEmpty, let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup, chatPresentationInterfaceState.interfaceState.messageActionsState.dismissedButtonKeyboardMessageId != message.id { accessoryItems.append(.inputButtons) } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift index 71007c4caa..a988e0bdc1 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputNodes.swift @@ -6,12 +6,24 @@ import Postbox import AccountContext import ChatPresentationInterfaceState -func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, inputMediaNode: ChatMediaInputNode?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?) -> ChatInputNode? { +func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentNode: ChatInputNode?, interfaceInteraction: ChatPanelInterfaceInteraction?, inputMediaNode: ChatMediaInputNode?, controllerInteraction: ChatControllerInteraction, inputPanelNode: ChatInputPanelNode?, makeMediaInputNode: () -> ChatInputNode?) -> ChatInputNode? { if !(inputPanelNode is ChatTextInputPanelNode) { return nil } switch chatPresentationInterfaceState.inputMode { - case .media: + case .media: + if "".isEmpty { + if let currentNode = currentNode as? ChatEntityKeyboardInputNode { + return currentNode + } else if let inputMediaNode = inputMediaNode { + return inputMediaNode + } else if let inputMediaNode = makeMediaInputNode() { + inputMediaNode.interfaceInteraction = interfaceInteraction + return inputMediaNode + } else { + return nil + } + } else { if let currentNode = currentNode as? ChatMediaInputNode { return currentNode } else if let inputMediaNode = inputMediaNode { @@ -39,19 +51,20 @@ func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: inputNode.interfaceInteraction = interfaceInteraction return inputNode } - case .inputButtons: - if chatPresentationInterfaceState.forceInputCommandsHidden { - return nil - } else { - if let currentNode = currentNode as? ChatButtonKeyboardInputNode { - return currentNode - } else { - let inputNode = ChatButtonKeyboardInputNode(context: context, controllerInteraction: controllerInteraction) - inputNode.interfaceInteraction = interfaceInteraction - return inputNode - } - } - case .none, .text: + } + case .inputButtons: + if chatPresentationInterfaceState.forceInputCommandsHidden { return nil + } else { + if let currentNode = currentNode as? ChatButtonKeyboardInputNode { + return currentNode + } else { + let inputNode = ChatButtonKeyboardInputNode(context: context, controllerInteraction: controllerInteraction) + inputNode.interfaceInteraction = interfaceInteraction + return inputNode + } + } + case .none, .text: + return nil } } diff --git a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift index bb3373b2b6..40faa41d68 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputNode.swift @@ -1579,9 +1579,9 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode, animationNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode, imageNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode.view, imageNode.bounds) } } f(.default) @@ -1593,9 +1593,9 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode, animationNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode, imageNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode.view, imageNode.bounds) } } f(.default) @@ -1731,9 +1731,9 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode, animationNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode, imageNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode.view, imageNode.bounds) } } f(.default) @@ -1745,9 +1745,9 @@ final class ChatMediaInputNode: ChatInputNode { }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode, animationNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode, imageNode.bounds) + let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode.view, imageNode.bounds) } } f(.default) diff --git a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift index f06f403ee9..b18d419cf6 100644 --- a/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/ChatMediaInputStickerGridItem.swift @@ -412,7 +412,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { if let interfaceInteraction = self.interfaceInteraction, let (_, item, _) = self.currentState, case .ended = recognizer.state { if let isLocked = self.isLocked, isLocked { } else { - let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, false, nil, false, self, self.bounds) + let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, false, nil, false, self.view, self.bounds) self.imageNode.layer.animateAlpha(from: 0.5, to: 1.0, duration: 1.0) } } diff --git a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift index eaa4b84307..ba675ff969 100644 --- a/submodules/TelegramUI/Sources/ChatRecentActionsController.swift +++ b/submodules/TelegramUI/Sources/ChatRecentActionsController.swift @@ -153,6 +153,8 @@ final class ChatRecentActionsController: TelegramBaseController { }, displayCopyProtectionTip: { _, _ in }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in + }, insertText: { _ in + }, backwardsDeleteText: { }, chatController: { return nil }, statuses: nil) diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 06ddecf35f..c4798cd366 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -177,7 +177,7 @@ private func calclulateTextFieldMinHeight(_ presentationInterfaceState: ChatPres return result } -private func calculateTextFieldRealInsets(_ presentationInterfaceState: ChatPresentationInterfaceState) -> UIEdgeInsets { +private func calculateTextFieldRealInsets(presentationInterfaceState: ChatPresentationInterfaceState, accessoryButtonsWidth: CGFloat) -> UIEdgeInsets { let baseFontSize = max(minInputFontSize, presentationInterfaceState.fontSize.baseDisplaySize) let top: CGFloat let bottom: CGFloat @@ -194,7 +194,11 @@ private func calculateTextFieldRealInsets(_ presentationInterfaceState: ChatPres top = 0.0 bottom = 0.0 } - return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: 0.0) + + var right: CGFloat = 0.0 + right += accessoryButtonsWidth + + return UIEdgeInsets(top: 4.5 + top, left: 0.0, bottom: 5.5 + bottom, right: right) } private var currentTextInputBackgroundImage: (UIColor, UIColor, CGFloat, UIImage)? @@ -667,7 +671,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputBackgroundNode.isUserInteractionEnabled = true self.textInputBackgroundNode.view.addGestureRecognizer(recognizer) - /*if let presentationContext = presentationContext { + if let presentationContext = presentationContext { self.emojiViewProvider = { [weak self, weak presentationContext] emoji in guard let strongSelf = self, let presentationContext = presentationContext, let presentationInterfaceState = strongSelf.presentationInterfaceState, let context = strongSelf.context, let file = strongSelf.context?.animatedEmojiStickers[emoji]?.first?.file else { return UIView() @@ -675,7 +679,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return EmojiTextAttachmentView(context: context, file: file, cache: presentationContext.animationCache, renderer: presentationContext.animationRenderer, placeholderColor: presentationInterfaceState.theme.chat.inputPanel.inputTextColor.withAlphaComponent(0.12)) } - }*/ + } } required init?(coder aDecoder: NSCoder) { @@ -725,26 +729,26 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.view.disablesInteractiveTransitionGestureRecognizer = true self.textInputNode = textInputNode + var accessoryButtonsWidth: CGFloat = 0.0 + var firstButton = true + for (_, button) in self.accessoryItemButtons { + if firstButton { + firstButton = false + accessoryButtonsWidth += accessoryButtonInset + } else { + accessoryButtonsWidth += accessoryButtonSpacing + } + accessoryButtonsWidth += button.buttonWidth + } + if let presentationInterfaceState = self.presentationInterfaceState { refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) - textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState) + textInputNode.textContainerInset = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) } if !self.textInputContainer.bounds.size.width.isZero { let textInputFrame = self.textInputContainer.frame - var accessoryButtonsWidth: CGFloat = 0.0 - var firstButton = true - for (_, button) in self.accessoryItemButtons { - if firstButton { - firstButton = false - accessoryButtonsWidth += accessoryButtonInset - } else { - accessoryButtonsWidth += accessoryButtonSpacing - } - accessoryButtonsWidth += button.buttonWidth - } - textInputNode.frame = CGRect(origin: CGPoint(x: self.textInputViewInternalInsets.left, y: self.textInputViewInternalInsets.top), size: CGSize(width: textInputFrame.size.width - (self.textInputViewInternalInsets.left + self.textInputViewInternalInsets.right), height: textInputFrame.size.height - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) textInputNode.view.layoutIfNeeded() self.updateSpoiler() @@ -1647,7 +1651,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var textInputViewRealInsets = UIEdgeInsets() if let presentationInterfaceState = self.presentationInterfaceState { - textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState) + textInputViewRealInsets = calculateTextFieldRealInsets(presentationInterfaceState: presentationInterfaceState, accessoryButtonsWidth: accessoryButtonsWidth) } let textInputFrame = CGRect(x: leftInset + textFieldInsets.left, y: textFieldInsets.top, width: baseWidth - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset, height: panelHeight - textFieldInsets.top - textFieldInsets.bottom) @@ -1717,7 +1721,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.slowmodePlaceholderNode?.isHidden = true } - var nextButtonTopRight = CGPoint(x: width - rightInset - textFieldInsets.right - accessoryButtonInset, y: minimalHeight - textFieldInsets.bottom - minimalInputHeight) + var nextButtonTopRight = CGPoint(x: width - rightInset - textFieldInsets.right - accessoryButtonInset, y: panelHeight - textFieldInsets.bottom - minimalInputHeight) for (_, button) in self.accessoryItemButtons.reversed() { let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight) button.updateLayout(size: buttonSize) @@ -2320,9 +2324,23 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { - self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in - return (.text, state.keyboardButtonsMessage?.id) - }) + guard let interfaceInteraction = self.interfaceInteraction, let presentationInterfaceState = self.presentationInterfaceState else { + return + } + + switch presentationInterfaceState.inputMode { + case .text: + break + case .media: + break + case .inputButtons, .none: + if self.textInputNode?.textView.inputView == nil { + interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.text, state.keyboardButtonsMessage?.id) + }) + } + } + self.inputMenu.activate() } @@ -2330,9 +2348,20 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage self.inputMenu.deactivate() - if let presentationInterfaceState = self.presentationInterfaceState, let peer = presentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil { - self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { _ in - return (.inputButtons, nil) + if let presentationInterfaceState = self.presentationInterfaceState { + if let peer = presentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil, presentationInterfaceState.keyboardButtonsMessage != nil { + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { _ in + return (.inputButtons, nil) + } + } else { + switch presentationInterfaceState.inputMode { + case .text, .media: + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { _ in + return (.none, nil) + } + default: + break + } } } } @@ -2698,6 +2727,20 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputNode?.becomeFirstResponder() } + func insertText(string: String) { + guard let textInputNode = self.textInputNode else { + return + } + textInputNode.textView.insertText(string) + } + + func backwardsDeleteText() { + guard let textInputNode = self.textInputNode else { + return + } + textInputNode.textView.deleteBackward() + } + @objc func expandButtonPressed() { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in if case let .media(mode, expanded, focused) = state.inputMode { diff --git a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift index dc168430ca..155e383412 100644 --- a/submodules/TelegramUI/Sources/DrawingStickersScreen.swift +++ b/submodules/TelegramUI/Sources/DrawingStickersScreen.swift @@ -40,7 +40,7 @@ private struct DrawingPaneArrangement { private final class DrawingStickersScreenNode: ViewControllerTracingNode { private let context: AccountContext private var presentationData: PresentationData - fileprivate var selectSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + fileprivate var selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private var searchItemContext = StickerPaneSearchGlobalItemContext() private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> @@ -98,7 +98,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { fileprivate var dismiss: (() -> Void)? - init(context: AccountContext, selectSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) { + init(context: AccountContext, selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) { self.context = context let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData @@ -106,7 +106,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode { self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) - var selectStickerImpl: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + var selectStickerImpl: ((FileMediaReference, UIView, CGRect) -> Bool)? self.controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in @@ -1306,7 +1306,7 @@ final class DrawingStickersScreen: ViewController, TGPhotoPaintStickersScreen { public var screenWillDisappear: (() -> Void)? private let context: AccountContext - var selectSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + var selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private var controllerNode: DrawingStickersScreenNode { return self.displayNode as! DrawingStickersScreenNode @@ -1322,7 +1322,7 @@ final class DrawingStickersScreen: ViewController, TGPhotoPaintStickersScreen { return self._ready } - public init(context: AccountContext, selectSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil) { + public init(context: AccountContext, selectSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil) { self.context = context self.selectSticker = selectSticker @@ -1362,10 +1362,10 @@ final class DrawingStickersScreen: ViewController, TGPhotoPaintStickersScreen { override public func loadDisplayNode() { self.displayNode = DrawingStickersScreenNode( context: self.context, - selectSticker: { [weak self] file, sourceNode, sourceRect in + selectSticker: { [weak self] file, sourceView, sourceRect in if let strongSelf = self, let selectSticker = strongSelf.selectSticker { (strongSelf.displayNode as! DrawingStickersScreenNode).animateOut() - return selectSticker(file, sourceNode, sourceRect) + return selectSticker(file, sourceView, sourceRect) } else { return false } diff --git a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift index 39986aa3ae..2c5e72a1c4 100644 --- a/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift +++ b/submodules/TelegramUI/Sources/FeaturedStickersScreen.swift @@ -192,7 +192,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { private let context: AccountContext private var presentationData: PresentationData private weak var controller: FeaturedStickersScreen? - private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private var searchItemContext = StickerPaneSearchGlobalItemContext() let gridNode: GridNode @@ -222,7 +222,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { } private var didSetReady: Bool = false - init(context: AccountContext, controller: FeaturedStickersScreen, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?) { + init(context: AccountContext, controller: FeaturedStickersScreen, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.controller = controller @@ -485,9 +485,9 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode, animationNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode, imageNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), imageNode.view, imageNode.bounds) } } f(.default) @@ -576,7 +576,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { menuItems = [ .action(ContextMenuActionItem(text: strongSelf.presentationData.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController, let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode, animationNode.bounds) + let _ = strongSelf.sendSticker?(.standalone(media: item.file), animationNode.view, animationNode.bounds) } f(.default) })), @@ -789,7 +789,7 @@ private final class FeaturedStickersScreenNode: ViewControllerTracingNode { final class FeaturedStickersScreen: ViewController { private let context: AccountContext fileprivate let highlightedPackId: ItemCollectionId? - private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private var controllerNode: FeaturedStickersScreenNode { return self.displayNode as! FeaturedStickersScreenNode @@ -805,7 +805,7 @@ final class FeaturedStickersScreen: ViewController { fileprivate var searchNavigationNode: SearchNavigationContentNode? - public init(context: AccountContext, highlightedPackId: ItemCollectionId?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? = nil) { + public init(context: AccountContext, highlightedPackId: ItemCollectionId?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? = nil) { self.context = context self.highlightedPackId = highlightedPackId self.sendSticker = sendSticker @@ -1034,7 +1034,7 @@ private enum FeaturedSearchEntry: Identifiable, Comparable { switch self { case let .sticker(_, code, stickerItem, theme): return StickerPaneSearchStickerItem(account: account, code: code, stickerItem: stickerItem, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { node, rect in - interaction.sendSticker(.standalone(media: stickerItem.file), node, rect) + interaction.sendSticker(.standalone(media: stickerItem.file), node.view, rect) }) case let .global(_, info, topItems, installed, topSeparator): return StickerPaneSearchGlobalItem(account: account, theme: theme, strings: strings, listAppearance: true, fillsRow: true, info: info, topItems: topItems, topSeparator: topSeparator, regularInsets: false, installed: installed, unread: false, open: { @@ -1080,7 +1080,7 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { private let inputNodeInteraction: ChatMediaInputNodeInteraction private var interaction: StickerPaneSearchInteraction? private weak var controller: FeaturedStickersScreen? - private let sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)? + private let sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)? private let itemContext: StickerPaneSearchGlobalItemContext private var theme: PresentationTheme @@ -1115,7 +1115,7 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { } var isActiveUpdated: (() -> Void)? - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, inputNodeInteraction: ChatMediaInputNodeInteraction, controller: FeaturedStickersScreen, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, itemContext: StickerPaneSearchGlobalItemContext) { + init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, inputNodeInteraction: ChatMediaInputNodeInteraction, controller: FeaturedStickersScreen, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, itemContext: StickerPaneSearchGlobalItemContext) { self.context = context self.inputNodeInteraction = inputNodeInteraction self.controller = controller @@ -1175,9 +1175,9 @@ private final class FeaturedPaneSearchContentNode: ASDisplayNode { |> deliverOnMainQueue).start(next: { _ in }) } - }, sendSticker: { [weak self] file, sourceNode, sourceRect in + }, sendSticker: { [weak self] file, sourceView, sourceRect in if let strongSelf = self { - let _ = strongSelf.sendSticker?(file, sourceNode, sourceRect) + let _ = strongSelf.sendSticker?(file, sourceView, sourceRect) } }, getItemIsPreviewed: { item in return inputNodeInteraction.previewedStickerPackItem == .pack(item) diff --git a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift index 23a4da53d9..ce97e1b83e 100644 --- a/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalListContextResultsChatInputContextPanelNode.swift @@ -162,9 +162,9 @@ final class HorizontalListContextResultsChatInputContextPanelNode: ChatInputCont f(.default) if let strongSelf = self { - let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.interfaceInteraction?.getNavigationController(), sendSticker: { file, sourceNode, sourceRect in + let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.interfaceInteraction?.getNavigationController(), sendSticker: { file, sourceView, sourceRect in if let strongSelf = self { - return strongSelf.interfaceInteraction?.sendSticker(file, false, sourceNode, sourceRect) ?? false + return strongSelf.interfaceInteraction?.sendSticker(file, false, sourceView, sourceRect) ?? false } else { return false } diff --git a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift index 479356050c..5385b98998 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickerGridItem.swift @@ -17,11 +17,11 @@ final class HorizontalStickerGridItem: GridItem { let file: TelegramMediaFile let theme: PresentationTheme let isPreviewed: (HorizontalStickerGridItem) -> Bool - let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Void + let sendSticker: (FileMediaReference, UIView, CGRect) -> Void let section: GridSection? = nil - init(account: Account, file: TelegramMediaFile, theme: PresentationTheme, isPreviewed: @escaping (HorizontalStickerGridItem) -> Bool, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Void) { + init(account: Account, file: TelegramMediaFile, theme: PresentationTheme, isPreviewed: @escaping (HorizontalStickerGridItem) -> Bool, sendSticker: @escaping (FileMediaReference, UIView, CGRect) -> Void) { self.account = account self.file = file self.theme = theme @@ -54,7 +54,7 @@ final class HorizontalStickerGridItemNode: GridItemNode { private let stickerFetchedDisposable = MetaDisposable() - var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)? + var sendSticker: ((FileMediaReference, UIView, CGRect) -> Void)? private var currentIsPreviewing: Bool = false @@ -238,7 +238,7 @@ final class HorizontalStickerGridItemNode: GridItemNode { @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { if let (_, item, _) = self.currentState, case .ended = recognizer.state { - self.sendSticker?(.standalone(media: item.file), self, self.bounds) + self.sendSticker?(.standalone(media: item.file), self.view, self.bounds) } } diff --git a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift index 4cbfdca401..1d99c75e84 100755 --- a/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/HorizontalStickersChatContextPanelNode.swift @@ -180,7 +180,7 @@ final class HorizontalStickersChatContextPanelNode: ChatInputContextPanelNode { .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode, itemNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode.view, itemNode.bounds) })), .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) diff --git a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift index 74f9b3eca9..ef2e3bd426 100644 --- a/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift +++ b/submodules/TelegramUI/Sources/InlineReactionSearchPanel.swift @@ -45,7 +45,7 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie var previewedStickerItem: StickerPackItem? var updateBackgroundOffset: ((CGFloat, Bool, ContainedViewLayoutTransition) -> Void)? - var sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Void)? + var sendSticker: ((FileMediaReference, UIView, CGRect) -> Void)? var getControllerInteraction: (() -> ChatControllerInteraction?)? @@ -111,9 +111,9 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, animationNode, animationNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, imageNode, imageNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, imageNode.view, imageNode.bounds) } } f(.default) @@ -125,9 +125,9 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, animationNode, animationNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, animationNode.view, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, imageNode, imageNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, imageNode.view, imageNode.bounds) } } f(.default) @@ -434,8 +434,8 @@ private final class InlineReactionSearchStickersNode: ASDisplayNode, UIScrollVie theme: self.theme, isPreviewed: { [weak self] item in return item.file.fileId == self?.previewedStickerItem?.file.fileId - }, sendSticker: { [weak self] file, node, rect in - self?.sendSticker?(file, node, rect) + }, sendSticker: { [weak self] file, view, rect in + self?.sendSticker?(file, view, rect) } ) itemNode = item.node(layout: GridNodeLayout( diff --git a/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift b/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift index 6ae0117529..a5bd2aa3c8 100644 --- a/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/Sources/LegacyInstantVideoController.swift @@ -120,7 +120,7 @@ func legacyInputMicPalette(from theme: PresentationTheme) -> TGModernConversatio return TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: theme.rootController.navigationBar.opaqueBackgroundColor, borderColor: inputPanelTheme.panelSeparatorColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor) } -func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, context: AccountContext, peerId: PeerId, slowmodeState: ChatSlowmodeState?, hasSchedule: Bool, send: @escaping (InstantVideoController, EnqueueMessage?) -> Void, displaySlowmodeTooltip: @escaping (ASDisplayNode, CGRect) -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void) -> InstantVideoController { +func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, context: AccountContext, peerId: PeerId, slowmodeState: ChatSlowmodeState?, hasSchedule: Bool, send: @escaping (InstantVideoController, EnqueueMessage?) -> Void, displaySlowmodeTooltip: @escaping (UIView, CGRect) -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void) -> InstantVideoController { let isSecretChat = peerId.namespace == Namespaces.Peer.SecretChat let legacyController = InstantVideoController(presentation: .custom, theme: theme) @@ -244,7 +244,7 @@ func legacyInstantVideoController(theme: PresentationTheme, panelFrame: CGRect, controller.displaySlowmodeTooltip = { [weak legacyController, weak controller] in if let legacyController = legacyController, let controller = controller { let rect = controller.frameForSendButton() - displaySlowmodeTooltip(legacyController.displayNode, rect) + displaySlowmodeTooltip(legacyController.displayNode.view, rect) } } legacyController.bindCaptureController(controller) diff --git a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift index 490c7bc021..cd1c1377ac 100644 --- a/submodules/TelegramUI/Sources/OpenResolvedUrl.swift +++ b/submodules/TelegramUI/Sources/OpenResolvedUrl.swift @@ -45,7 +45,7 @@ private func defaultNavigationForPeerId(_ peerId: PeerId?, navigation: ChatContr } } -func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)? = nil, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) { +func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)? = nil, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) { let updatedPresentationData: (initial: PresentationData, signal: Signal)? if case let .chat(_, maybeUpdatedPresentationData) = urlContext { updatedPresentationData = maybeUpdatedPresentationData diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 7b34ad0bcb..20756b514b 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -359,6 +359,8 @@ final class PeerInfoSelectionPanelNode: ASDisplayNode { displayCopyProtectionTip(node, save) }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in + }, insertText: { _ in + }, backwardsDeleteText: { }, chatController: { return nil }, statuses: nil) @@ -3357,8 +3359,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate transitionCompletion() }, presentStickers: { [weak self] completion in if let strongSelf = self { - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in - completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in + completion(fileReference.media, fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.controller?.present(controller, in: .window(.root)) @@ -6051,10 +6053,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate let paintStickersContext = LegacyPaintStickersContext(context: strongSelf.context) paintStickersContext.presentStickersController = { completion in - let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, node, rect in + let controller = DrawingStickersScreen(context: strongSelf.context, selectSticker: { fileReference, view, rect in let coder = PostboxEncoder() coder.encodeRootObject(fileReference.media) - completion?(coder.makeData(), fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, node.view, rect) + completion?(coder.makeData(), fileReference.media.isAnimatedSticker || fileReference.media.isVideoSticker, view, rect) return true }) strongSelf.controller?.present(controller, in: .window(.root)) diff --git a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift index cb19f498c2..b88389adb5 100644 --- a/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift +++ b/submodules/TelegramUI/Sources/PeerSelectionControllerNode.swift @@ -349,6 +349,8 @@ final class PeerSelectionControllerNode: ASDisplayNode { }, displayCopyProtectionTip: { _, _ in }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in + }, insertText: { _ in + }, backwardsDeleteText: { }, chatController: { return nil }, statuses: nil) diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index ec89f0bb4b..0771a98da8 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1201,7 +1201,7 @@ public final class SharedAccountContextImpl: SharedAccountContext { return resolveUrlImpl(context: context, peerId: peerId, url: url, skipUrlAuth: skipUrlAuth) } - public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, ASDisplayNode, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) { + public func openResolvedUrl(_ resolvedUrl: ResolvedUrl, context: AccountContext, urlContext: OpenURLContext, navigationController: NavigationController?, forceExternal: Bool, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, sendFile: ((FileMediaReference) -> Void)?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?, requestMessageActionUrlAuth: ((MessageActionUrlSubject) -> Void)?, joinVoiceChat: ((PeerId, String?, CachedChannelData.ActiveCall) -> Void)?, present: @escaping (ViewController, Any?) -> Void, dismissInput: @escaping () -> Void, contentContext: Any?) { openResolvedUrlImpl(resolvedUrl, context: context, urlContext: urlContext, navigationController: navigationController, forceExternal: forceExternal, openPeer: openPeer, sendFile: sendFile, sendSticker: sendSticker, requestMessageActionUrlAuth: requestMessageActionUrlAuth, joinVoiceChat: joinVoiceChat, present: present, dismissInput: dismissInput, contentContext: contentContext) } diff --git a/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift b/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift index 3f735b3a4c..581ac2be30 100644 --- a/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift +++ b/submodules/TelegramUI/Sources/StickerPaneSearchContentNode.swift @@ -19,10 +19,10 @@ import UndoUI final class StickerPaneSearchInteraction { let open: (StickerPackCollectionInfo) -> Void let install: (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void - let sendSticker: (FileMediaReference, ASDisplayNode, CGRect) -> Void + let sendSticker: (FileMediaReference, UIView, CGRect) -> Void let getItemIsPreviewed: (StickerPackItem) -> Bool - init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void, sendSticker: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { + init(open: @escaping (StickerPackCollectionInfo) -> Void, install: @escaping (StickerPackCollectionInfo, [ItemCollectionItem], Bool) -> Void, sendSticker: @escaping (FileMediaReference, UIView, CGRect) -> Void, getItemIsPreviewed: @escaping (StickerPackItem) -> Bool) { self.open = open self.install = install self.sendSticker = sendSticker @@ -100,7 +100,7 @@ private enum StickerSearchEntry: Identifiable, Comparable { switch self { case let .sticker(_, code, stickerItem, theme): return StickerPaneSearchStickerItem(account: account, code: code, stickerItem: stickerItem, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { node, rect in - interaction.sendSticker(.standalone(media: stickerItem.file), node, rect) + interaction.sendSticker(.standalone(media: stickerItem.file), node.view, rect) }) case let .global(_, info, topItems, installed, topSeparator): let itemContext = StickerPaneSearchGlobalItemContext() @@ -319,9 +319,9 @@ final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode { |> deliverOnMainQueue).start(next: { _ in }) } - }, sendSticker: { [weak self] file, sourceNode, sourceRect in + }, sendSticker: { [weak self] file, sourceView, sourceRect in if let strongSelf = self { - let _ = strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect) + let _ = strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceView, sourceRect) } }, getItemIsPreviewed: { item in return inputNodeInteraction.previewedStickerPackItem == .pack(item) diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift index a0055a4545..e474a5c6ee 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelItem.swift @@ -126,7 +126,7 @@ final class StickersChatInputContextPanelItemNode: ListViewItemNode { for i in 0 ..< self.nodes.count { if self.nodes[i].frame.contains(location) { let file = item.files[i] - let _ = item.interfaceInteraction.sendSticker(.standalone(media: file), true, self.nodes[i], self.nodes[i].bounds) + let _ = item.interfaceInteraction.sendSticker(.standalone(media: file), true, self.nodes[i].view, self.nodes[i].bounds) break } } diff --git a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift index b31262461c..8f23e7f235 100644 --- a/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/StickersChatInputContextPanelNode.swift @@ -136,7 +136,7 @@ final class StickersChatInputContextPanelNode: ChatInputContextPanelNode { .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_Send, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.default) - let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode, itemNode.bounds) + let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, false, nil, true, itemNode.view, itemNode.bounds) })), .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default)