diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 220a6c7933..f3b36166f6 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7940,6 +7940,9 @@ Sorry for the inconvenience."; "EmojiInput.PremiumEmojiToast.Text" = "Subscribe to Telegram Premium to unlock premium emoji."; "EmojiInput.PremiumEmojiToast.Action" = "More"; +"EmojiInput.PremiumEmojiToast.TryText" = "Try sending these emojis in **Saved Messages** for free to test."; +"EmojiInput.PremiumEmojiToast.TryAction" = "Open"; + "StickerPacks.DeleteEmojiPacksConfirmation_1" = "Delete 1 Emoji Pack"; "StickerPacks.DeleteEmojiPacksConfirmation_any" = "Delete %@ Emoji Packs"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 758799c0e7..cc5b909c56 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -369,6 +369,11 @@ public enum ChatLocation: Equatable { case feed(id: Int32) } +public enum ChatControllerActivateInput { + case text + case entityInput +} + public final class NavigateToChatControllerParams { public let navigationController: NavigationController public let chatController: ChatController? @@ -379,7 +384,7 @@ public final class NavigateToChatControllerParams { public let botStart: ChatControllerInitialBotStart? public let attachBotStart: ChatControllerInitialAttachBotStart? public let updateTextInputState: ChatTextInputState? - public let activateInput: Bool + public let activateInput: ChatControllerActivateInput? public let keepStack: NavigateToChatKeepStack public let useExisting: Bool public let useBackAnimation: Bool @@ -398,7 +403,7 @@ public final class NavigateToChatControllerParams { public let setupController: (ChatController) -> Void public let completion: (ChatController) -> Void - public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: Bool = false, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [PeerId] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, completion: @escaping (ChatController) -> Void = { _ in }) { + public init(navigationController: NavigationController, chatController: ChatController? = nil, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic = Atomic(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, updateTextInputState: ChatTextInputState? = nil, activateInput: ChatControllerActivateInput? = nil, keepStack: NavigateToChatKeepStack = .default, useExisting: Bool = true, useBackAnimation: Bool = false, purposefulAction: (() -> Void)? = nil, scrollToEndIfExists: Bool = false, activateMessageSearch: (ChatSearchDomain, String)? = nil, peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, reportReason: ReportReason? = nil, animated: Bool = true, options: NavigationAnimationOptions = [], parentGroupId: PeerGroupId? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [PeerId] = [], changeColors: Bool = false, setupController: @escaping (ChatController) -> Void = { _ in }, completion: @escaping (ChatController) -> Void = { _ in }) { self.navigationController = navigationController self.chatController = chatController self.chatLocationContextHolder = chatLocationContextHolder diff --git a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm index 4e7f6c3935..b9a977c80e 100644 --- a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm +++ b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm @@ -240,6 +240,10 @@ return [super textInputMode]; } +- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated { + [super scrollRectToVisible:rect animated:false]; +} + #endif - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer @@ -756,7 +760,16 @@ NSRange range = [self selectedRange]; range.location = range.location + range.length - 1; range.length = 1; - [self.textView scrollRangeToVisible:range]; + + UITextPosition *caretPosition = [self.textView positionFromPosition:self.textView.beginningOfDocument offset:range.location]; + if (caretPosition) { + CGRect caretRect = [self.textView caretRectForPosition:caretPosition]; + caretRect.origin.y -= self.textView.contentInset.top; + caretRect.size.height += self.textView.contentInset.top + self.textView.contentInset.bottom + 4.0f; + [self.textView scrollRectToVisible:caretRect animated:false]; + } + + //[self.textView scrollRangeToVisible:range]; } #pragma mark - Keyboard diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 8bfcc7fd59..07b889783b 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -917,7 +917,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController scrollToEndIfExists = true } - strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: peer.id), activateInput: activateInput && !peer.isDeleted, scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, chatListFilter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.id, completion: { [weak self] controller in + strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(id: peer.id), activateInput: (activateInput && !peer.isDeleted) ? .text : nil, scrollToEndIfExists: scrollToEndIfExists, animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, chatListFilter: strongSelf.chatListDisplayNode.containerNode.currentItemNode.chatListFilter?.id, completion: { [weak self] controller in self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true) if let promoInfo = promoInfo { switch promoInfo { diff --git a/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift b/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift index 417499b57e..c97b5b013b 100644 --- a/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift +++ b/submodules/ChatMessageInteractiveMediaBadge/Sources/ChatMessageInteractiveMediaBadge.swift @@ -167,8 +167,8 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode { sizeNode.bounds = CGRect(origin: CGPoint(), size: sizeFrame.size) let previousFrame = sizeNode.frame - if previousFrame.center.y != sizeFrame.center.y { - textTransition.updatePosition(node: sizeNode, position: sizeFrame.center) + if previousFrame.midY != sizeFrame.midY { + textTransition.updatePosition(node: sizeNode, position: CGPoint(x: sizeFrame.midX, y: sizeFrame.midY)) } else { sizeNode.layer.removeAllAnimations() sizeNode.frame = sizeFrame @@ -178,7 +178,7 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode { let sizeSize = sizeNode.frame.size let sizeFrame = CGRect(x: active ? 42.0 : 7.0, y: active ? 19.0 : 2.0, width: sizeSize.width, height: sizeSize.height) sizeNode.bounds = CGRect(origin: CGPoint(), size: sizeFrame.size) - textTransition.updatePosition(node: sizeNode, position: sizeFrame.center) + textTransition.updatePosition(node: sizeNode, position: CGPoint(x: sizeFrame.midX, y: sizeFrame.midY)) transition.updateAlpha(node: sizeNode, alpha: 0.0) } @@ -190,7 +190,7 @@ public final class ChatMessageInteractiveMediaBadge: ASDisplayNode { let durationFrame = CGRect(x: active ? 42.0 : 7.0, y: active ? 6.0 : 2.0 + UIScreenPixel, width: durationSize.width, height: durationSize.height) self.durationNode.bounds = CGRect(origin: CGPoint(), size: durationFrame.size) - textTransition.updatePosition(node: self.durationNode, position: durationFrame.center) + textTransition.updatePosition(node: self.durationNode, position: CGPoint(x: durationFrame.midX, y: durationFrame.midY)) let iconNode: ASImageNode if let current = self.iconNode { diff --git a/submodules/DatePickerNode/Sources/DatePickerNode.swift b/submodules/DatePickerNode/Sources/DatePickerNode.swift index 49c79ab0d9..54d9a85b3f 100644 --- a/submodules/DatePickerNode/Sources/DatePickerNode.swift +++ b/submodules/DatePickerNode/Sources/DatePickerNode.swift @@ -744,7 +744,7 @@ public final class DatePickerNode: ASDisplayNode { self.monthTextNode.frame = monthTextFrame let monthArrowFrame = CGRect(x: monthTextFrame.maxX + 10.0, y: monthTextFrame.minY + 4.0, width: 7.0, height: 12.0) - self.monthArrowNode.position = monthArrowFrame.center + self.monthArrowNode.position = CGPoint(x: monthArrowFrame.midX, y: monthArrowFrame.midY) self.monthArrowNode.bounds = CGRect(origin: CGPoint(), size: monthArrowFrame.size) transition.updateTransformRotation(node: self.monthArrowNode, angle: self.state.displayingMonthSelection ? CGFloat.pi / 2.0 : 0.0) diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 5cbc895b8c..3f4e52b5d7 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -3,6 +3,12 @@ import UIKit import AsyncDisplayKit import ObjCRuntimeUtils +extension CGRect { + var center: CGPoint { + return CGPoint(x: self.midX, y: self.midY) + } +} + public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable { case linear case easeInOut diff --git a/submodules/Display/Source/UIKitUtils.swift b/submodules/Display/Source/UIKitUtils.swift index 5c6021eb33..2d868f8b1a 100644 --- a/submodules/Display/Source/UIKitUtils.swift +++ b/submodules/Display/Source/UIKitUtils.swift @@ -591,10 +591,6 @@ public extension CGRect { var bottomRight: CGPoint { return CGPoint(x: self.maxX, y: self.maxY) } - - var center: CGPoint { - return CGPoint(x: self.midX, y: self.midY) - } } public extension CGPoint { diff --git a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift index 4b6e0de4a2..7df4d9995d 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerScrubbingNode.swift @@ -838,10 +838,10 @@ public final class MediaPlayerScrubbingNode: ASDisplayNode { let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: floor((bounds.size.height - node.lineHeight) / 2.0)), size: CGSize(width: bounds.size.width, height: node.lineHeight)) let foregroundContentFrame = CGRect(origin: CGPoint(), size: CGSize(width: backgroundFrame.size.width, height: backgroundFrame.size.height)) - node.backgroundNode.position = backgroundFrame.center + node.backgroundNode.position = CGPoint(x: backgroundFrame.midX, y: backgroundFrame.midY) node.backgroundNode.bounds = CGRect(origin: CGPoint(), size: backgroundFrame.size) - node.foregroundContentNode.position = foregroundContentFrame.center + node.foregroundContentNode.position = CGPoint(x: foregroundContentFrame.midX, y: foregroundContentFrame.midY) node.foregroundContentNode.bounds = CGRect(origin: CGPoint(), size: foregroundContentFrame.size) node.bufferingNode.frame = backgroundFrame diff --git a/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift b/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift index 6011d9ca07..57dca9563a 100644 --- a/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift +++ b/submodules/PasscodeUI/Sources/PasscodeEntryControllerNode.swift @@ -13,6 +13,12 @@ import PasscodeInputFieldNode import MonotonicTime import GradientBackground +private extension CGRect { + var center: CGPoint { + return CGPoint(x: self.midX, y: self.midY) + } +} + private let titleFont = Font.regular(20.0) private let subtitleFont = Font.regular(15.0) private let buttonFont = Font.regular(17.0) diff --git a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift index f1c4ecefe6..ac5fcf9e02 100644 --- a/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift +++ b/submodules/RadialStatusNode/Sources/RadialStatusSecretTimeoutContentNode.swift @@ -122,7 +122,7 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { if self.sparks { let lineWidth: CGFloat = 1.75 - let center = bounds.center + let center = CGPoint(x: bounds.midX, y: bounds.midY) let radius: CGFloat = (bounds.size.width - lineWidth - 2.5 * 2.0) * 0.5 let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * self.progress @@ -211,7 +211,7 @@ final class RadialStatusSecretTimeoutContentNode: RadialStatusContentNode { context.setLineJoin(.miter) context.setMiterLimit(10.0) - let center = bounds.center + let center = CGPoint(x: bounds.midX, y: bounds.midY) let radius: CGFloat = (bounds.size.width - lineWidth - 2.5 * 2.0) * 0.5 let startAngle: CGFloat = -CGFloat.pi / 2.0 diff --git a/submodules/StatisticsUI/Sources/GroupStatsController.swift b/submodules/StatisticsUI/Sources/GroupStatsController.swift index e2978dfa68..2bac63f92e 100644 --- a/submodules/StatisticsUI/Sources/GroupStatsController.swift +++ b/submodules/StatisticsUI/Sources/GroupStatsController.swift @@ -880,7 +880,7 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat let _ = (context.account.postbox.loadedPeerWithId(participantPeerId) |> take(1) |> deliverOnMainQueue).start(next: { peer in - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, updateTextInputState: nil, activateInput: false, keepStack: .always, useExisting: false, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: (.member(peer), ""), animated: true)) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, chatController: nil, context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, updateTextInputState: nil, keepStack: .always, useExisting: false, purposefulAction: nil, scrollToEndIfExists: false, activateMessageSearch: (.member(peer), ""), animated: true)) }) } } diff --git a/submodules/TabBarUI/Sources/TabBarNode.swift b/submodules/TabBarUI/Sources/TabBarNode.swift index 6897ab4a11..5867b6b865 100644 --- a/submodules/TabBarUI/Sources/TabBarNode.swift +++ b/submodules/TabBarUI/Sources/TabBarNode.swift @@ -7,6 +7,12 @@ import UIKitRuntimeUtils import AnimatedStickerNode import TelegramAnimatedStickerNode +private extension CGRect { + var center: CGPoint { + return CGPoint(x: self.midX, y: self.midY) + } +} + private let separatorHeight: CGFloat = 1.0 / UIScreen.main.scale private func tabBarItemImage(_ image: UIImage?, title: String, backgroundColor: UIColor, tintColor: UIColor, horizontal: Bool, imageMode: Bool, centered: Bool = false) -> (UIImage, CGFloat) { let font = horizontal ? Font.regular(13.0) : Font.medium(10.0) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 4d9a0a1245..be2389ee34 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -289,6 +289,7 @@ swift_library( "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", "//submodules/TelegramUI/Components/ChatInputPanelContainer:ChatInputPanelContainer", "//submodules/TelegramUI/Components/TextNodeWithEntities:TextNodeWithEntities", + "//submodules/TelegramUI/Components/EmojiSuggestionsComponent:EmojiSuggestionsComponent", "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", "//submodules/Media/ConvertOpusToAAC:ConvertOpusToAAC", "//submodules/Media/LocalAudioTranscription:LocalAudioTranscription", diff --git a/submodules/TelegramUI/Components/ChatInputPanelContainer/Sources/ChatInputPanelContainer.swift b/submodules/TelegramUI/Components/ChatInputPanelContainer/Sources/ChatInputPanelContainer.swift index a333939a18..56280fc3c9 100644 --- a/submodules/TelegramUI/Components/ChatInputPanelContainer/Sources/ChatInputPanelContainer.swift +++ b/submodules/TelegramUI/Components/ChatInputPanelContainer/Sources/ChatInputPanelContainer.swift @@ -100,6 +100,12 @@ private final class ExpansionPanRecognizer: UIGestureRecognizer, UIGestureRecogn if let scrollView = traceScrollView(view: view, point: point).0 ?? hitView.flatMap(traceScrollViewUp) { if scrollView is ListViewScroller || scrollView is GridNodeScrollerView || scrollView.asyncdisplaykit_node is ASScrollNode { found = false + } else if let textView = scrollView as? UITextView { + if textView.contentSize.height <= textView.bounds.height { + found = true + } else { + found = false + } } else { found = true } @@ -206,7 +212,7 @@ private final class ExpansionPanRecognizer: UIGestureRecognizer, UIGestureRecogn } } -public final class ChatInputPanelContainer: SparseNode, UIScrollViewDelegate { +public final class ChatInputPanelContainer: SparseNode { public var expansionUpdated: ((ContainedViewLayoutTransition) -> Void)? private var expansionRecognizer: ExpansionPanRecognizer? diff --git a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/BUILD b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/BUILD new file mode 100644 index 0000000000..e2620d3942 --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/BUILD @@ -0,0 +1,28 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "EmojiSuggestionsComponent", + module_name = "EmojiSuggestionsComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", + "//submodules/AccountContext:AccountContext", + "//submodules/Postbox:Postbox", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/TextFormat:TextFormat", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift new file mode 100644 index 0000000000..94c87feb27 --- /dev/null +++ b/submodules/TelegramUI/Components/EmojiSuggestionsComponent/Sources/EmojiSuggestionsComponent.swift @@ -0,0 +1,363 @@ +import Foundation +import UIKit +import SwiftSignalKit +import Display +import AnimationCache +import MultiAnimationRenderer +import ComponentFlow +import AccountContext +import TelegramCore +import Postbox +import TelegramPresentationData +import EmojiTextAttachmentView +import TextFormat + +public final class EmojiSuggestionsComponent: Component { + public typealias EnvironmentType = Empty + + public static func suggestionData(context: AccountContext, query: String) -> Signal<[TelegramMediaFile], NoError> { + let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium + } + |> distinctUntilChanged + + return combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), + context.account.viewTracker.featuredEmojiPacks(), + hasPremium + ) + |> take(1) + |> map { view, featuredEmojiPacks, hasPremium -> [TelegramMediaFile] in + var result: [TelegramMediaFile] = [] + + var existingIds = Set() + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + for attribute in item.file.attributes { + switch attribute { + case let .CustomEmoji(_, alt, _): + if alt == query { + if !item.file.isPremiumEmoji || hasPremium { + if !existingIds.contains(item.file.fileId) { + existingIds.insert(item.file.fileId) + result.append(item.file) + } + } + } + default: + break + } + } + } + + for featuredPack in featuredEmojiPacks { + for item in featuredPack.topItems { + for attribute in item.file.attributes { + switch attribute { + case let .CustomEmoji(_, alt, _): + if alt == query { + if !item.file.isPremiumEmoji || hasPremium { + if !existingIds.contains(item.file.fileId) { + existingIds.insert(item.file.fileId) + result.append(item.file) + } + } + } + default: + break + } + } + } + } + + return result + } + } + + public let context: AccountContext + public let theme: PresentationTheme + public let animationCache: AnimationCache + public let animationRenderer: MultiAnimationRenderer + public let files: [TelegramMediaFile] + public let action: (TelegramMediaFile) -> Void + + public init( + context: AccountContext, + theme: PresentationTheme, + animationCache: AnimationCache, + animationRenderer: MultiAnimationRenderer, + files: [TelegramMediaFile], + action: @escaping (TelegramMediaFile) -> Void + ) { + self.context = context + self.theme = theme + self.animationCache = animationCache + self.animationRenderer = animationRenderer + self.files = files + self.action = action + } + + public static func ==(lhs: EmojiSuggestionsComponent, rhs: EmojiSuggestionsComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.animationCache !== rhs.animationCache { + return false + } + if lhs.animationRenderer !== rhs.animationRenderer { + return false + } + if lhs.files != rhs.files { + return false + } + return true + } + + public final class View: UIView, UIScrollViewDelegate { + private struct ItemLayout: Equatable { + let spacing: CGFloat + let itemSize: CGFloat + let verticalInset: CGFloat + let itemCount: Int + let contentSize: CGSize + let sideInset: CGFloat + + init(itemCount: Int) { + #if DEBUG + //var itemCount = itemCount + //itemCount = 100 + #endif + + self.spacing = 9.0 + self.itemSize = 38.0 + self.verticalInset = 5.0 + self.sideInset = 5.0 + self.itemCount = itemCount + + self.contentSize = CGSize(width: self.sideInset * 2.0 + CGFloat(self.itemCount - 1) * self.spacing + CGFloat(self.itemCount) * self.itemSize, height: self.itemSize + self.verticalInset * 2.0) + } + + func frame(at index: Int) -> CGRect { + return CGRect(origin: CGPoint(x: self.sideInset + CGFloat(index) * (self.spacing + self.itemSize), y: self.verticalInset), size: CGSize(width: self.itemSize, height: self.itemSize)) + } + } + + private let backgroundLayer: SimpleShapeLayer + private let scrollView: UIScrollView + + private var component: EmojiSuggestionsComponent? + private var itemLayout: ItemLayout? + private var ignoreScrolling: Bool = false + + private var visibleLayers: [MediaId: InlineStickerItemLayer] = [:] + + override init(frame: CGRect) { + self.backgroundLayer = SimpleShapeLayer() + self.backgroundLayer.shadowColor = UIColor(white: 0.0, alpha: 1.0).cgColor + self.backgroundLayer.shadowOffset = CGSize(width: 0.0, height: 2.0) + self.backgroundLayer.shadowRadius = 15.0 + self.backgroundLayer.shadowOpacity = 0.15 + + self.scrollView = UIScrollView() + + super.init(frame: frame) + + self.disablesInteractiveTransitionGestureRecognizer = true + self.disablesInteractiveKeyboardGestureRecognizer = true + + self.scrollView.layer.anchorPoint = CGPoint() + self.scrollView.delaysContentTouches = false + self.scrollView.clipsToBounds = false + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.showsVerticalScrollIndicator = false + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.alwaysBounceHorizontal = false + self.scrollView.scrollsToTop = false + self.scrollView.delegate = self + self.scrollView.clipsToBounds = true + + self.layer.addSublayer(self.backgroundLayer) + 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 { + let location = recognizer.location(in: self.scrollView) + if self.scrollView.bounds.contains(location) { + var closestFile: (file: TelegramMediaFile, distance: CGFloat)? + for (_, itemLayer) in self.visibleLayers { + guard let file = itemLayer.file else { + continue + } + let distance = abs(location.x - itemLayer.position.x) + if let (_, currentDistance) = closestFile { + if distance < currentDistance { + closestFile = (file, distance) + } + } else { + closestFile = (file, distance) + } + } + if let (file, _) = closestFile { + self.component?.action(file) + } + } + } + } + + public func scrollViewDidScroll(_ scrollView: UIScrollView) { + if !self.ignoreScrolling { + self.updateVisibleItems(synchronousLoad: false) + } + } + + private func updateVisibleItems(synchronousLoad: Bool) { + guard let component = self.component, let itemLayout = self.itemLayout else { + return + } + + let visibleBounds = self.scrollView.bounds + + var visibleIds = Set() + for i in 0 ..< component.files.count { + let itemFrame = itemLayout.frame(at: i) + if visibleBounds.intersects(itemFrame) { + let item = component.files[i] + visibleIds.insert(item.fileId) + + let itemLayer: InlineStickerItemLayer + if let current = self.visibleLayers[item.fileId] { + itemLayer = current + } else { + itemLayer = InlineStickerItemLayer( + context: component.context, + attemptSynchronousLoad: synchronousLoad, + emoji: ChatTextInputTextCustomEmojiAttribute(stickerPack: nil, fileId: item.fileId.id, file: item), + file: item, + cache: component.animationCache, + renderer: component.animationRenderer, + placeholderColor: component.theme.list.mediaPlaceholderColor, + pointSize: itemFrame.size + ) + self.visibleLayers[item.fileId] = itemLayer + self.scrollView.layer.addSublayer(itemLayer) + } + + itemLayer.frame = itemFrame + + itemLayer.isVisibleForAnimations = true + } + } + + var removedIds: [MediaId] = [] + for (id, itemLayer) in self.visibleLayers { + if !visibleIds.contains(id) { + itemLayer.removeFromSuperlayer() + removedIds.append(id) + } + } + for id in removedIds { + self.visibleLayers.removeValue(forKey: id) + } + } + + public func adjustBackground(relativePositionX: CGFloat) { + let size = self.bounds.size + if size.width.isZero { + return + } + + let radius: CGFloat = 10.0 + let notchSize = CGSize(width: 19.0, height: 7.5) + + let path = CGMutablePath() + path.move(to: CGPoint(x: radius, y: 0.0)) + path.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: 0.0, y: radius), radius: radius) + path.addLine(to: CGPoint(x: 0.0, y: size.height - notchSize.height - radius)) + path.addArc(tangent1End: CGPoint(x: 0.0, y: size.height - notchSize.height), tangent2End: CGPoint(x: radius, y: size.height - notchSize.height), radius: radius) + + let notchBase = CGPoint(x: min(size.width - radius - notchSize.width, max(radius, floor(relativePositionX - notchSize.width / 2.0))), y: size.height - notchSize.height) + path.addLine(to: notchBase) + path.addCurve(to: CGPoint(x: notchBase.x + 7.49968, y: notchBase.y + 5.32576), control1: CGPoint(x: notchBase.x + 2.10085, y: notchBase.y + 0.0), control2: CGPoint(x: notchBase.x + 5.41005, y: notchBase.y + 3.11103)) + path.addCurve(to: CGPoint(x: notchBase.x + 8.95665, y: notchBase.y + 6.61485), control1: CGPoint(x: notchBase.x + 8.2352, y: notchBase.y + 6.10531), control2: CGPoint(x: notchBase.x + 8.60297, y: notchBase.y + 6.49509)) + path.addCurve(to: CGPoint(x: notchBase.x + 9.91544, y: notchBase.y + 6.61599), control1: CGPoint(x: notchBase.x + 9.29432, y: notchBase.y + 6.72919), control2: CGPoint(x: notchBase.x + 9.5775, y: notchBase.y + 6.72953)) + path.addCurve(to: CGPoint(x: notchBase.x + 11.3772, y: notchBase.y + 5.32853), control1: CGPoint(x: notchBase.x + 10.2694, y: notchBase.y + 6.49707), control2: CGPoint(x: notchBase.x + 10.6387, y: notchBase.y + 6.10756)) + path.addCurve(to: CGPoint(x: notchBase.x + 19.0, y: notchBase.y + 0.0), control1: CGPoint(x: notchBase.x + 13.477, y: notchBase.y + 3.11363), control2: CGPoint(x: notchBase.x + 16.817, y: notchBase.y + 0.0)) + + path.addLine(to: CGPoint(x: size.width - radius, y: size.height - notchSize.height)) + path.addArc(tangent1End: CGPoint(x: size.width, y: size.height - notchSize.height), tangent2End: CGPoint(x: size.width, y: size.height - notchSize.height - radius), radius: radius) + path.addLine(to: CGPoint(x: size.width, y: radius)) + path.addArc(tangent1End: CGPoint(x: size.width, y: 0.0), tangent2End: CGPoint(x: size.width - radius, y: 0.0), radius: radius) + path.addLine(to: CGPoint(x: radius, y: 0.0)) + + self.backgroundLayer.frame = CGRect(origin: CGPoint(), size: size) + self.backgroundLayer.path = path + self.backgroundLayer.shadowPath = path + } + + func update(component: EmojiSuggestionsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let height: CGFloat = 54.0 + + if self.component?.theme !== component.theme { + self.backgroundLayer.fillColor = component.theme.list.plainBackgroundColor.cgColor + } + var resetScrollingPosition = false + if self.component?.files != component.files { + resetScrollingPosition = true + } + + self.component = component + + let itemLayout = ItemLayout(itemCount: component.files.count) + self.itemLayout = itemLayout + + let size = CGSize(width: min(availableSize.width, itemLayout.contentSize.width), height: height) + + let scrollFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: itemLayout.contentSize.height)) + + self.ignoreScrolling = true + if self.scrollView.frame != scrollFrame { + self.scrollView.frame = scrollFrame + } + if self.scrollView.contentSize != itemLayout.contentSize { + self.scrollView.contentSize = itemLayout.contentSize + } + if resetScrollingPosition { + self.scrollView.contentOffset = CGPoint() + } + self.ignoreScrolling = false + + self.updateVisibleItems(synchronousLoad: resetScrollingPosition) + + return size + } + } + + 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/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift index 34593fb7a0..ed731a7939 100644 --- a/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift +++ b/submodules/TelegramUI/Components/EmojiTextAttachmentView/Sources/EmojiTextAttachmentView.swift @@ -90,7 +90,7 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { private var isDisplayingPlaceholder: Bool = false - private var file: TelegramMediaFile? + public private(set) var file: TelegramMediaFile? private var infoDisposable: Disposable? private var disposable: Disposable? private var fetchDisposable: Disposable? @@ -186,10 +186,6 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { let placeholderColor = self.placeholderColor self.loadDisposable = self.renderer.loadFirstFrame(target: self, cache: self.cache, itemId: file.resource.id.stringRepresentation, size: self.pixelSize, fetch: animationCacheFetchFile(context: self.context, resource: .media(media: .standalone(media: file), resource: file.resource), type: AnimationCacheAnimationType(file: file), keyframeOnly: true), completion: { [weak self] result, isFinal in if !result { - if !isFinal { - return - } - MultiAnimationRendererImpl.firstFrameQueue.async { let image = generateStickerPlaceholderImage(data: file.immediateThumbnailData, size: pointSize, scale: min(2.0, UIScreenScale), imageSize: file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0), backgroundColor: nil, foregroundColor: placeholderColor) @@ -201,7 +197,10 @@ public final class InlineStickerItemLayer: MultiAnimationRenderTarget { strongSelf.contents = image.cgImage strongSelf.isDisplayingPlaceholder = true } - strongSelf.loadAnimation() + + if isFinal { + strongSelf.loadAnimation() + } } } } else { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index c878b124ca..5c1d217a0e 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -28,7 +28,6 @@ swift_library( "//submodules/TelegramUI/Components/LottieAnimationCache:LottieAnimationCache", "//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", - "//submodules/TelegramUI/Components/MultiVideoRenderer:MultiVideoRenderer", "//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView", "//submodules/SoftwareVideo:SoftwareVideo", "//submodules/ShimmerEffect:ShimmerEffect", @@ -42,7 +41,7 @@ swift_library( "//submodules/Components/MultilineTextComponent:MultilineTextComponent", "//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", - "//submodules/MurMurHash32:MurMurHash32", + "//submodules/LocalizedPeerData:LocalizedPeerData", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 48e649a274..990e8efccf 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -1524,6 +1524,7 @@ public final class EmojiPagerContentComponent: Component { public let id: AnyHashable public let context: AccountContext + public let avatarPeer: EnginePeer? public let animationCache: AnimationCache public let animationRenderer: MultiAnimationRenderer public let inputInteractionHolder: InputInteractionHolder @@ -1533,6 +1534,7 @@ public final class EmojiPagerContentComponent: Component { public init( id: AnyHashable, context: AccountContext, + avatarPeer: EnginePeer?, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, inputInteractionHolder: InputInteractionHolder, @@ -1541,6 +1543,7 @@ public final class EmojiPagerContentComponent: Component { ) { self.id = id self.context = context + self.avatarPeer = avatarPeer self.animationCache = animationCache self.animationRenderer = animationRenderer self.inputInteractionHolder = inputInteractionHolder @@ -1558,6 +1561,9 @@ public final class EmojiPagerContentComponent: Component { if lhs.context !== rhs.context { return false } + if lhs.avatarPeer != rhs.avatarPeer { + return false + } if lhs.animationCache !== rhs.animationCache { return false } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 6c20ad4936..ef138bfe6b 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -10,6 +10,7 @@ import BlurredBackgroundComponent import BundleIconComponent import AudioToolbox import SwiftSignalKit +import LocalizedPeerData public final class EntityKeyboardChildEnvironment: Equatable { public let theme: PresentationTheme @@ -333,29 +334,47 @@ public final class EntityKeyboardComponent: Component { for itemGroup in stickerContent.itemGroups { if let id = itemGroup.supergroupId.base as? String { - let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ - "saved": .saved, - "recent": .recent, - "premium": .premium - ] - let titleMapping: [String: String] = [ - "saved": component.strings.Stickers_Favorites, - "recent": component.strings.Stickers_Recent, - "premium": component.strings.EmojiInput_PanelTitlePremium - ] - if let icon = iconMapping[id], let title = titleMapping[id] { - topStickerItems.append(EntityKeyboardTopPanelComponent.Item( - id: itemGroup.supergroupId, - isReorderable: false, - content: AnyComponent(EntityKeyboardIconTopPanelComponent( - icon: icon, - theme: component.theme, - title: title, - pressed: { [weak self] in - self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) - } + if id == "peerSpecific" { + if let avatarPeer = stickerContent.avatarPeer { + topStickerItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: false, + content: AnyComponent(EntityKeyboardAvatarTopPanelComponent( + context: stickerContent.context, + peer: avatarPeer, + theme: component.theme, + title: avatarPeer.compactDisplayTitle, + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) + } + )) )) - )) + } + } else { + let iconMapping: [String: EntityKeyboardIconTopPanelComponent.Icon] = [ + "saved": .saved, + "recent": .recent, + "premium": .premium + ] + let titleMapping: [String: String] = [ + "saved": component.strings.Stickers_Favorites, + "recent": component.strings.Stickers_Recent, + "premium": component.strings.EmojiInput_PanelTitlePremium + ] + if let icon = iconMapping[id], let title = titleMapping[id] { + topStickerItems.append(EntityKeyboardTopPanelComponent.Item( + id: itemGroup.supergroupId, + isReorderable: false, + content: AnyComponent(EntityKeyboardIconTopPanelComponent( + icon: icon, + theme: component.theme, + title: title, + pressed: { [weak self] in + self?.scrollToItemGroup(contentId: "stickers", groupId: itemGroup.supergroupId, subgroupId: nil) + } + )) + )) + } } } else { if !itemGroup.items.isEmpty { diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift index de8e241f2e..caf0c2a867 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboardTopPanelComponent.swift @@ -12,7 +12,7 @@ import MultiAnimationRenderer import AccountContext import MultilineTextComponent import LottieAnimationComponent -import MurMurHash32 +import AvatarNode final class EntityKeyboardAnimationTopPanelComponent: Component { typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment @@ -424,6 +424,137 @@ final class EntityKeyboardIconTopPanelComponent: Component { } } +final class EntityKeyboardAvatarTopPanelComponent: Component { + typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment + + let context: AccountContext + let peer: EnginePeer + let theme: PresentationTheme + let title: String + let pressed: () -> Void + + init( + context: AccountContext, + peer: EnginePeer, + theme: PresentationTheme, + title: String, + pressed: @escaping () -> Void + ) { + self.context = context + self.peer = peer + self.theme = theme + self.title = title + self.pressed = pressed + } + + static func ==(lhs: EntityKeyboardAvatarTopPanelComponent, rhs: EntityKeyboardAvatarTopPanelComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.peer != rhs.peer { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.title != rhs.title { + return false + } + + return true + } + + final class View: UIView { + let avatarNode: AvatarNode + var component: EntityKeyboardAvatarTopPanelComponent? + var titleView: ComponentView? + + override init(frame: CGRect) { + self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) + + super.init(frame: frame) + + self.addSubnode(self.avatarNode) + + 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 { + self.component?.pressed() + } + } + + func update(component: EntityKeyboardAvatarTopPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + let itemEnvironment = environment[EntityKeyboardTopPanelItemEnvironment.self].value + + self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: component.peer) + self.component = component + + let nativeIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 44.0, height: 44.0) : CGSize(width: 24.0, height: 24.0) + let boundingIconSize: CGSize = itemEnvironment.isExpanded ? CGSize(width: 38.0, height: 38.0) : CGSize(width: 24.0, height: 24.0) + let iconSize = boundingIconSize + + let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) / 2.0), y: floor((nativeIconSize.height - iconSize.height) / 2.0)), size: iconSize) + + transition.containedViewLayoutTransition.updateFrame(node: self.avatarNode, frame: iconFrame) + + if itemEnvironment.isExpanded { + let titleView: ComponentView + if let current = self.titleView { + titleView = current + } else { + titleView = ComponentView() + self.titleView = titleView + } + let titleSize = titleView.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.title, font: Font.regular(10.0), textColor: component.theme.chat.inputPanel.primaryTextColor)), + insets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0) + )), + environment: {}, + containerSize: CGSize(width: 62.0, height: 100.0) + ) + if let view = titleView.view { + if view.superview == nil { + view.alpha = 0.0 + self.addSubview(view) + } + view.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) / 2.0), y: availableSize.height - titleSize.height - 1.0), size: titleSize) + transition.setAlpha(view: view, alpha: 1.0) + } + } else if let titleView = self.titleView { + self.titleView = nil + if let view = titleView.view { + if !transition.animation.isImmediate { + view.alpha = 0.0 + view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.08, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + } + + return availableSize + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + final class EntityKeyboardStaticStickersPanelComponent: Component { typealias EnvironmentType = EntityKeyboardTopPanelItemEnvironment diff --git a/submodules/TelegramUI/Components/MultiVideoRenderer/BUILD b/submodules/TelegramUI/Components/MultiVideoRenderer/BUILD deleted file mode 100644 index e6fa67cd5f..0000000000 --- a/submodules/TelegramUI/Components/MultiVideoRenderer/BUILD +++ /dev/null @@ -1,20 +0,0 @@ -load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") - -swift_library( - name = "MultiVideoRenderer", - module_name = "MultiVideoRenderer", - srcs = glob([ - "Sources/**/*.swift", - ]), - copts = [ - "-warnings-as-errors", - ], - deps = [ - "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/Display:Display", - "//submodules/SoftwareVideo:SoftwareVideo", - ], - visibility = [ - "//visibility:public", - ], -) diff --git a/submodules/TelegramUI/Components/MultiVideoRenderer/Sources/MultiVideoRenderer.swift b/submodules/TelegramUI/Components/MultiVideoRenderer/Sources/MultiVideoRenderer.swift deleted file mode 100644 index 359224b564..0000000000 --- a/submodules/TelegramUI/Components/MultiVideoRenderer/Sources/MultiVideoRenderer.swift +++ /dev/null @@ -1,399 +0,0 @@ -import Foundation -import UIKit -import SwiftSignalKit -import Display -import SoftwareVideo - -/*public protocol MultiVideoRenderer: AnyObject { - func add(groupId: String, target: MultiVideoRenderTarget, itemId: String, size: CGSize, source: @escaping (@escaping (String) -> Void) -> Disposable) -> Disposable -} - -open class MultiVideoRenderTarget: SimpleLayer { - fileprivate let deinitCallbacks = Bag<() -> Void>() - fileprivate let updateStateCallbacks = Bag<() -> Void>() - - public final var shouldBeAnimating: Bool = false { - didSet { - if self.shouldBeAnimating != oldValue { - for f in self.updateStateCallbacks.copyItems() { - f() - } - } - } - } - - deinit { - for f in self.deinitCallbacks.copyItems() { - f() - } - } - - open func updateDisplayPlaceholder(displayPlaceholder: Bool) { - } -} - -private final class ItemVideoContext { - static let queue = Queue(name: "ItemVideoContext", qos: .default) - - private let stateUpdated: () -> Void - - private var disposable: Disposable? - private var displayLink: ConstantDisplayLinkAnimator? - private var frameManager: SoftwareVideoLayerFrameManager? - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - self.stateUpdated() - } - } - } - - let targets = Bag>() - - init(itemId: String, source: @escaping (@escaping (String) -> Void) -> Disposable, stateUpdated: @escaping () -> Void) { - self.stateUpdated = stateUpdated - - self.disposable = source({ [weak self] in - Queue.mainQueue().async { - guard let strongSelf = self else { - return - } - //strongSelf.frameManager = SoftwareVideoLayerFrameManager(account: <#T##Account#>, fileReference: <#T##FileMediaReference#>, layerHolder: <#T##SampleBufferLayer#>) - strongSelf.updateIsPlaying() - - if result.item == nil { - for target in strongSelf.targets.copyItems() { - if let target = target.value { - target.updateDisplayPlaceholder(displayPlaceholder: true) - } - } - } - } - }) - } - - deinit { - self.disposable?.dispose() - self.displayLink?.invalidate() - } - - func updateAddedTarget(target: MultiAnimationRenderTarget) { - if let item = self.item, let currentFrameGroup = self.currentFrameGroup { - 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 - } - } - - self.updateIsPlaying() - } - - func updateIsPlaying() { - var isPlaying = true - if self.item == nil { - isPlaying = false - } - - var shouldBeAnimating = false - for target in self.targets.copyItems() { - if let target = target.value { - if target.shouldBeAnimating { - shouldBeAnimating = true - break - } - } - } - if !shouldBeAnimating { - isPlaying = false - } - - self.isPlaying = isPlaying - } - - func animationTick() -> LoadFrameGroupTask? { - return self.update(advanceFrame: true) - } - - private func update(advanceFrame: Bool) -> LoadFrameGroupTask? { - guard let item = self.item else { - return nil - } - - let currentFrame = self.frameIndex % item.numFrames - - if let currentFrameGroup = self.currentFrameGroup, currentFrameGroup.frameRange.contains(currentFrame) { - } 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: frameSkip) - - return { - guard let strongSelf = self else { - return - } - - strongSelf.isLoadingFrameGroup = false - - if let currentFrameGroup = currentFrameGroup { - strongSelf.currentFrameGroup = currentFrameGroup - for target in strongSelf.targets.copyItems() { - target.value?.contents = currentFrameGroup.image.cgImage - } - - let _ = strongSelf.update(advanceFrame: false) - } - } - }) - } - - if advanceFrame { - self.frameIndex += self.frameSkip - } - - if let currentFrameGroup = self.currentFrameGroup, let contentsRect = currentFrameGroup.contentsRect(index: currentFrame) { - for target in self.targets.copyItems() { - if let target = target.value { - target.updateDisplayPlaceholder(displayPlaceholder: false) - target.contentsRect = contentsRect - } - } - } - - return nil - } -} - -public final class MultiAnimationRendererImpl: MultiAnimationRenderer { - private final class GroupContext { - private var frameSkip: Int - private let stateUpdated: () -> Void - - private var itemContexts: [String: ItemAnimationContext] = [:] - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - self.stateUpdated() - } - } - } - - init(frameSkip: Int, stateUpdated: @escaping () -> Void) { - self.frameSkip = frameSkip - self.stateUpdated = stateUpdated - } - - 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, size: size, frameSkip: self.frameSkip, fetch: fetch, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.itemContexts[itemId] = itemContext - } - - let index = itemContext.targets.add(Weak(target)) - itemContext.updateAddedTarget(target: target) - - let deinitIndex = target.deinitCallbacks.add { [weak self, weak itemContext] in - Queue.mainQueue().async { - guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else { - return - } - itemContext.targets.remove(index) - if itemContext.targets.isEmpty { - strongSelf.itemContexts.removeValue(forKey: itemId) - } - } - } - - let updateStateIndex = target.updateStateCallbacks.add { [weak itemContext] in - guard let itemContext = itemContext else { - return - } - itemContext.updateIsPlaying() - } - - return ActionDisposable { [weak self, weak itemContext, weak target] in - guard let strongSelf = self, let itemContext = itemContext, strongSelf.itemContexts[itemId] === itemContext else { - return - } - if let target = target { - target.deinitCallbacks.remove(deinitIndex) - target.updateStateCallbacks.remove(updateStateIndex) - } - itemContext.targets.remove(index) - if itemContext.targets.isEmpty { - strongSelf.itemContexts.removeValue(forKey: 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 - } - - target.contents = frameGroup.image.cgImage - - return true - } else { - return false - } - } - - private func updateIsPlaying() { - var isPlaying = false - for (_, itemContext) in self.itemContexts { - if itemContext.isPlaying { - isPlaying = true - break - } - } - - self.isPlaying = isPlaying - } - - func animationTick() -> [LoadFrameGroupTask] { - var tasks: [LoadFrameGroupTask] = [] - for (_, itemContext) in self.itemContexts { - if itemContext.isPlaying { - if let task = itemContext.animationTick() { - tasks.append(task) - } - } - } - - return tasks - } - } - - private var groupContexts: [String: GroupContext] = [:] - private var frameSkip: Int - private var displayLink: ConstantDisplayLinkAnimator? - - private(set) var isPlaying: Bool = false { - didSet { - if self.isPlaying != oldValue { - if self.isPlaying { - if self.displayLink == nil { - self.displayLink = ConstantDisplayLinkAnimator { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.animationTick() - } - self.displayLink?.frameInterval = self.frameSkip - self.displayLink?.isPaused = false - } - } else { - if let displayLink = self.displayLink { - self.displayLink = nil - displayLink.invalidate() - } - } - } - } - } - - public init() { - if !ProcessInfo.processInfo.isLowPowerModeEnabled && ProcessInfo.processInfo.activeProcessorCount > 2 { - self.frameSkip = 1 - } else { - self.frameSkip = 2 - } - } - - 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(frameSkip: self.frameSkip, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContexts[groupId] = groupContext - } - - 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, size: CGSize) -> Bool { - let groupContext: GroupContext - if let current = self.groupContexts[groupId] { - groupContext = current - } else { - groupContext = GroupContext(frameSkip: self.frameSkip, stateUpdated: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.updateIsPlaying() - }) - self.groupContexts[groupId] = groupContext - } - - return groupContext.loadFirstFrameSynchronously(target: target, cache: cache, itemId: itemId, size: size) - } - - private func updateIsPlaying() { - var isPlaying = false - for (_, groupContext) in self.groupContexts { - if groupContext.isPlaying { - isPlaying = true - break - } - } - - self.isPlaying = isPlaying - } - - private func animationTick() { - var tasks: [LoadFrameGroupTask] = [] - for (_, groupContext) in self.groupContexts { - if groupContext.isPlaying { - tasks.append(contentsOf: groupContext.animationTick()) - } - } - - if !tasks.isEmpty { - ItemAnimationContext.queue.async { - var completions: [() -> Void] = [] - for task in tasks { - let complete = task.task() - completions.append(complete) - } - - if !completions.isEmpty { - Queue.mainQueue().async { - for completion in completions { - completion() - } - } - } - } - } - } -} -*/ diff --git a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift index 8ffe7cf143..178e56efa0 100644 --- a/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift +++ b/submodules/TelegramUI/Components/TextNodeWithEntities/Sources/TextNodeWithEntities.swift @@ -9,6 +9,12 @@ import AnimationCache import MultiAnimationRenderer import TelegramCore +private extension CGRect { + var center: CGPoint { + return CGPoint(x: self.midX, y: self.midY) + } +} + private final class InlineStickerItem: Hashable { let emoji: ChatTextInputTextCustomEmojiAttribute let file: TelegramMediaFile? diff --git a/submodules/TelegramUI/Sources/ApplicationContext.swift b/submodules/TelegramUI/Sources/ApplicationContext.swift index 2f254b9681..9687cfcc40 100644 --- a/submodules/TelegramUI/Sources/ApplicationContext.swift +++ b/submodules/TelegramUI/Sources/ApplicationContext.swift @@ -849,7 +849,7 @@ final class AuthorizedApplicationContext { if visiblePeerId != peerId || messageId != nil { if self.rootController.rootTabController != nil { - self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: .peer(id: peerId), subject: messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) }, activateInput: activateInput)) + self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: .peer(id: peerId), subject: messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) }, activateInput: activateInput ? .text : nil)) } else { self.scheduledOpenChatWithPeerId = (peerId, messageId, activateInput) } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 6f9f259009..9d4514d4a0 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -401,7 +401,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G private var willAppear = false private var didAppear = false - private var scheduledActivateInput = false + private var scheduledActivateInput: ChatControllerActivateInput? private var raiseToListen: RaiseToListenManager? private var voicePlaylistDidEndTimestamp: Double = 0.0 @@ -7168,7 +7168,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - strongSelf.chatDisplayNode.openStickers() + strongSelf.chatDisplayNode.openStickers(beginWithEmoji: false) strongSelf.mediaRecordingModeTooltipController?.dismissImmediately() }, editMessage: { [weak self] in if let strongSelf = self, let editMessage = strongSelf.presentationInterfaceState.interfaceState.editMessage { @@ -7192,7 +7192,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if let value = value as? ChatTextInputTextCustomEmojiAttribute { if let file = value.file { inlineStickers[file.fileId] = file - if file.isPremiumEmoji && !strongSelf.presentationInterfaceState.isPremium { + if file.isPremiumEmoji && !strongSelf.presentationInterfaceState.isPremium && strongSelf.chatLocation.peerId != strongSelf.context.account.peerId { if firstLockedPremiumEmoji == nil { firstLockedPremiumEmoji = file } @@ -9252,11 +9252,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - if self.scheduledActivateInput { - self.scheduledActivateInput = false + if let scheduledActivateInput = scheduledActivateInput, case .text = scheduledActivateInput { + self.scheduledActivateInput = nil self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - return state.updatedInputMode({ _ in .text }) + return state.updatedInputMode({ _ in + switch scheduledActivateInput { + case .text: + return .text + case .entityInput: + return .media(mode: .other, expanded: nil, focused: false) + } + }) }) } @@ -9691,12 +9698,19 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - if self.scheduledActivateInput { - self.scheduledActivateInput = false + if let scheduledActivateInput = self.scheduledActivateInput { + self.scheduledActivateInput = nil - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - return state.updatedInputMode({ _ in .text }) - }) + switch scheduledActivateInput { + case .text: + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + return state.updatedInputMode({ _ in + return .text + }) + }) + case .entityInput: + self.chatDisplayNode.openStickers(beginWithEmoji: true) + } } if let snapshotState = self.storedAnimateFromSnapshotState { @@ -14118,7 +14132,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G subject = nil } - context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: chatLocation, chatLocationContextHolder: result.contextHolder, subject: subject, activateInput: result.isEmpty, keepStack: .always)) + context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: chatLocation, chatLocationContextHolder: result.contextHolder, subject: subject, activateInput: result.isEmpty ? .text : nil, keepStack: .always)) subscriber.putCompletion() }, error: { _ in let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 } @@ -16098,13 +16112,25 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - func activateInput() { + func activateInput(type: ChatControllerActivateInput) { if self.didAppear { - self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in - return state.updatedInputMode({ _ in .text }) - }) + switch type { + case .text: + self.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in + return state.updatedInputMode({ _ in + switch type { + case .text: + return .text + case .entityInput: + return .media(mode: .other, expanded: nil, focused: false) + } + }) + }) + case .entityInput: + self.chatDisplayNode.openStickers(beginWithEmoji: true) + } } else { - self.scheduledActivateInput = true + self.scheduledActivateInput = type } } diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 559e83389f..810a28551a 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -105,6 +105,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var navigationModalFrame: NavigationModalFrame? let inputPanelContainerNode: ChatInputPanelContainer + private let inputPanelOverlayNode: SparseNode private let inputPanelClippingNode: SparseNode private let inputPanelBackgroundNode: NavigationBackgroundNode private var intrinsicInputPanelBackgroundNodeSize: CGSize? @@ -121,7 +122,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var inputPanelNode: ChatInputPanelNode? private(set) var inputPanelOverscrollNode: ChatInputPanelOverscrollNode? - private weak var currentDismissedInputPanelNode: ASDisplayNode? + private weak var currentDismissedInputPanelNode: ChatInputPanelNode? private var secondaryInputPanelNode: ChatInputPanelNode? private(set) var accessoryPanelNode: AccessoryPanelNode? private var inputContextPanelNode: ChatInputContextPanelNode? @@ -245,6 +246,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { private var lastSendTimestamp = 0.0 + private var openStickersBeginWithEmoji: Bool = false private var openStickersDisposable: Disposable? private var displayVideoUnmuteTipDisposable: Disposable? @@ -388,6 +390,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.loadingNode = ChatLoadingNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper, bubbleCorners: self.chatPresentationInterfaceState.bubbleCorners) self.inputPanelContainerNode = ChatInputPanelContainer() + self.inputPanelOverlayNode = SparseNode() self.inputPanelClippingNode = SparseNode() if case let .color(color) = self.chatPresentationInterfaceState.chatWallpaper, UIColor(rgb: color).isEqual(self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColorNoWallpaper) { @@ -547,6 +550,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.addSubnode(self.inputContextOverTextPanelContainer) self.inputPanelContainerNode.addSubnode(self.inputPanelClippingNode) + self.inputPanelContainerNode.addSubnode(self.inputPanelOverlayNode) self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundNode) self.inputPanelClippingNode.addSubnode(self.inputPanelBackgroundSeparatorNode) self.inputPanelBackgroundNode.addSubnode(self.inputPanelBottomBackgroundSeparatorNode) @@ -1091,7 +1095,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { var immediatelyLayoutSecondaryInputPanelAndAnimateAppearance = false var inputPanelNodeHandlesTransition = false - var dismissedInputPanelNode: ASDisplayNode? + var dismissedInputPanelNode: ChatInputPanelNode? var dismissedSecondaryInputPanelNode: ASDisplayNode? var dismissedAccessoryPanelNode: AccessoryPanelNode? var dismissedInputContextPanelNode: ChatInputContextPanelNode? @@ -1123,6 +1127,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if inputPanelNode.supernode !== self { immediatelyLayoutInputPanelAndAnimateAppearance = true self.inputPanelClippingNode.insertSubnode(inputPanelNode, aboveSubnode: self.inputPanelBackgroundNode) + + if let viewForOverlayContent = inputPanelNode.viewForOverlayContent { + self.inputPanelOverlayNode.view.addSubview(viewForOverlayContent) + } } } else { let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: layout.additionalInsets, maxHeight: layout.size.height - insets.top - inputPanelBottomInset - 120.0, isSecondary: false, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: self.inputPanelContainerNode.expansionFraction == 1.0) @@ -1650,6 +1658,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputPanelContainerNode.update(size: layout.size, scrollableDistance: max(0.0, maximumInputNodeHeight - layout.standardInputHeight), isExpansionEnabled: isInputExpansionEnabled, transition: transition) transition.updatePosition(node: self.inputPanelClippingNode, position: CGRect(origin: apparentInputBackgroundFrame.origin, size: layout.size).center, beginWithCurrentState: true) transition.updateBounds(node: self.inputPanelClippingNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: layout.size), beginWithCurrentState: true) + transition.updatePosition(node: self.inputPanelOverlayNode, position: CGRect(origin: apparentInputBackgroundFrame.origin, size: layout.size).center, beginWithCurrentState: true) + transition.updateBounds(node: self.inputPanelOverlayNode, bounds: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: layout.size), beginWithCurrentState: true) transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame, beginWithCurrentState: true) transition.updateFrame(node: self.contentDimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: apparentInputBackgroundFrame.origin.y))) @@ -1802,6 +1812,14 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { transition.updateFrame(node: inputPanelNode, frame: apparentInputPanelFrame) transition.updateAlpha(node: inputPanelNode, alpha: 1.0) } + + if let viewForOverlayContent = inputPanelNode.viewForOverlayContent { + if inputPanelNodeHandlesTransition { + viewForOverlayContent.frame = apparentInputPanelFrame + } else { + transition.updateFrame(view: viewForOverlayContent, frame: apparentInputPanelFrame) + } + } } if let dismissedInputPanelNode = dismissedInputPanelNode, dismissedInputPanelNode !== self.secondaryInputPanelNode { @@ -1832,6 +1850,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { alphaCompleted = true completed() }) + + dismissedInputPanelNode.viewForOverlayContent?.removeFromSuperview() } if let dismissedSecondaryInputPanelNode = dismissedSecondaryInputPanelNode, dismissedSecondaryInputPanelNode !== self.inputPanelNode { @@ -2407,11 +2427,12 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { context: self.context, currentInputData: inputMediaNodeData, updatedInputData: self.inputMediaNodeDataPromise.get(), - defaultToEmojiTab: !self.chatPresentationInterfaceState.interfaceState.effectiveInputState.inputText.string.isEmpty, + defaultToEmojiTab: !self.chatPresentationInterfaceState.interfaceState.effectiveInputState.inputText.string.isEmpty || self.openStickersBeginWithEmoji, controllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction, chatPeerId: peerId ) + self.openStickersBeginWithEmoji = false return inputNode } @@ -2700,6 +2721,28 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { break } + var maybeDismissOverlayContent = true + if let inputNode = self.inputNode, inputNode.bounds.contains(self.view.convert(point, to: inputNode.view)) { + if let externalTopPanelContainer = inputNode.externalTopPanelContainer { + if externalTopPanelContainer.hitTest(self.view.convert(point, to: externalTopPanelContainer), with: nil) != nil { + maybeDismissOverlayContent = true + } else { + maybeDismissOverlayContent = false + } + } else { + maybeDismissOverlayContent = false + } + } + + if let inputPanelNode = self.inputPanelNode, let viewForOverlayContent = inputPanelNode.viewForOverlayContent { + if let result = viewForOverlayContent.hitTest(self.view.convert(point, to: viewForOverlayContent), with: event) { + return result + } + if maybeDismissOverlayContent { + viewForOverlayContent.maybeDismissContent(point: self.view.convert(point, to: viewForOverlayContent)) + } + } + return nil } @@ -2851,7 +2894,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { } } - func openStickers() { + func openStickers(beginWithEmoji: Bool) { + self.openStickersBeginWithEmoji = beginWithEmoji + if let inputMediaNode = self.inputMediaNode { if self.openStickersDisposable == nil { self.openStickersDisposable = (inputMediaNode.ready @@ -2916,7 +2961,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { if let value = value as? ChatTextInputTextCustomEmojiAttribute { if let file = value.file { inlineStickers[file.fileId] = file - if file.isPremiumEmoji && !self.chatPresentationInterfaceState.isPremium { + if file.isPremiumEmoji && !self.chatPresentationInterfaceState.isPremium && self.chatPresentationInterfaceState.chatLocation.peerId != self.context.account.peerId { if firstLockedPremiumEmoji == nil { firstLockedPremiumEmoji = file } diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index 1d5ab52c24..4208cf1102 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -86,16 +86,24 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } - static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, areCustomEmojiEnabled: Bool) -> Signal { - let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> map { peer -> Bool in - guard case let .user(user) = peer else { - return false + static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal { + let hasPremium: Signal + if premiumIfSavedMessages, let chatPeerId = chatPeerId, chatPeerId == context.account.peerId { + hasPremium = .single(true) + } else { + hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> Bool in + guard case let .user(user) = peer else { + return false + } + return user.isPremium } - return user.isPremium + |> distinctUntilChanged } - |> distinctUntilChanged - + return hasPremium + } + + static func emojiInputData(context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled @@ -103,7 +111,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { let emojiItems: Signal = combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.LocalRecentEmoji], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), - hasPremium, + ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true), context.account.viewTracker.featuredEmojiPacks() ) |> map { view, hasPremium, featuredEmojiPacks -> EmojiPagerContentComponent in @@ -305,6 +313,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return EmojiPagerContentComponent( id: "emoji", context: context, + avatarPeer: nil, animationCache: animationCache, animationRenderer: animationRenderer, inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), @@ -340,15 +349,6 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled - let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> map { peer -> Bool in - guard case let .user(user) = peer else { - return false - } - return user.isPremium - } - |> distinctUntilChanged - let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: { return TempBox.shared.tempFile(fileName: "file").path }) @@ -359,21 +359,64 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { animationRenderer = MultiAnimationRendererImpl() //} - let emojiItems = emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, areCustomEmojiEnabled: areCustomEmojiEnabled) + let emojiItems = emojiInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, isStandalone: false, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: chatPeerId) let stickerNamespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] let stickerOrderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudPremiumStickers] + struct PeerSpecificPackData: Equatable { + var info: StickerPackCollectionInfo + var items: [StickerPackItem] + var peer: EnginePeer + + static func ==(lhs: PeerSpecificPackData, rhs: PeerSpecificPackData) -> Bool { + if lhs.info.id != rhs.info.id { + return false + } + if lhs.items != rhs.items { + return false + } + if lhs.peer != rhs.peer { + return false + } + + return true + } + } + + let peerSpecificPack: Signal + if let chatPeerId = chatPeerId { + peerSpecificPack = combineLatest( + context.engine.peers.peerSpecificStickerPack(peerId: chatPeerId), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId)) + ) + |> map { packData, peer -> PeerSpecificPackData? in + guard let peer = peer else { + return nil + } + + guard let (info, items) = packData.packInfo else { + return nil + } + + return PeerSpecificPackData(info: info, items: items.compactMap { $0 as? StickerPackItem }, peer: peer) + } + |> distinctUntilChanged + } else { + peerSpecificPack = .single(nil) + } + let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings let stickerItems: Signal = combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000), - hasPremium, + ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false), context.account.viewTracker.featuredStickerPacks(), context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))), - ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager) + ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager), + peerSpecificPack ) - |> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks -> EmojiPagerContentComponent in + |> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks, peerSpecificPack -> EmojiPagerContentComponent in struct ItemGroup { var supergroupId: AnyHashable var id: AnyHashable @@ -548,6 +591,37 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } } + var avatarPeer: EnginePeer? + if let peerSpecificPack = peerSpecificPack { + avatarPeer = peerSpecificPack.peer + + var processedIds = Set() + for item in peerSpecificPack.items { + if isPremiumDisabled && item.file.isPremiumSticker { + continue + } + if processedIds.contains(item.file.fileId) { + continue + } + processedIds.insert(item.file.fileId) + + let resultItem = EmojiPagerContentComponent.Item( + animationData: EntityKeyboardAnimationData(file: item.file), + itemFile: item.file, + staticEmoji: nil, + subgroupId: nil + ) + + let groupId = "peerSpecific" + if let groupIndex = itemGroupIndexById[groupId] { + itemGroups[groupIndex].items.append(resultItem) + } else { + itemGroupIndexById[groupId] = itemGroups.count + itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.peer.compactDisplayTitle, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, headerItem: nil, items: [resultItem])) + } + } + } + for entry in view.entries { guard let item = entry.item as? StickerPackItem else { continue @@ -651,6 +725,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return EmojiPagerContentComponent( id: "stickers", context: context, + avatarPeer: avatarPeer, animationCache: animationCache, animationRenderer: animationRenderer, inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), @@ -1075,18 +1150,10 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer() - let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> map { peer -> Bool in - guard case let .user(user) = peer else { - return false - } - return user.isPremium - } - |> distinctUntilChanged - + var premiumToastCounter = 0 self.emojiInputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self, weak interfaceInteraction, weak controllerInteraction] _, item, _, _, _ in - let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in + let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: true) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in guard let strongSelf = self, let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else { return } @@ -1114,23 +1181,48 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: presentationData.strings.EmojiInput_PremiumEmojiToast_Text, undoText: presentationData.strings.EmojiInput_PremiumEmojiToast_Action, customAction: { [weak controllerInteraction] in + + premiumToastCounter += 1 + let suggestSavedMessages = premiumToastCounter % 2 == 0 + let text: String + let actionTitle: String + if suggestSavedMessages { + text = presentationData.strings.EmojiInput_PremiumEmojiToast_TryText + actionTitle = presentationData.strings.EmojiInput_PremiumEmojiToast_TryAction + } else { + text = presentationData.strings.EmojiInput_PremiumEmojiToast_Text + actionTitle = presentationData.strings.EmojiInput_PremiumEmojiToast_Action + } + + let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, title: nil, text: text, undoText: actionTitle, customAction: { [weak controllerInteraction] in guard let controllerInteraction = controllerInteraction else { return } - var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: { - let controller = PremiumIntroScreen(context: context, source: .animatedEmoji) - replaceImpl?(controller) - }) - replaceImpl = { [weak controller] c in - controller?.replace(with: c) + if suggestSavedMessages, let navigationController = controllerInteraction.navigationController() { + context.sharedContext.navigateToChatController(NavigateToChatControllerParams( + navigationController: navigationController, + chatController: nil, + context: context, + chatLocation: .peer(id: context.account.peerId), + subject: nil, + updateTextInputState: nil, + activateInput: .entityInput, + keepStack: .always, + completion: { _ in + }) + ) + } else { + var replaceImpl: ((ViewController) -> Void)? + let controller = PremiumDemoScreen(context: context, subject: .animatedEmoji, action: { + let controller = PremiumIntroScreen(context: context, source: .animatedEmoji) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + controllerInteraction.navigationController()?.pushViewController(controller) } - controllerInteraction.navigationController()?.pushViewController(controller) - - /*let controller = PremiumIntroScreen(context: context, source: .stickers) - controllerInteraction.navigationController()?.pushViewController(controller)*/ }), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in return false }) strongSelf.currentUndoOverlayController = controller controllerInteraction.presentController(controller, nil) @@ -1247,7 +1339,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { self.stickerInputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak controllerInteraction, weak interfaceInteraction] groupId, item, view, rect, layer in - let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in + let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in guard let controllerInteraction = controllerInteraction, let interfaceInteraction = interfaceInteraction else { return } @@ -1401,6 +1493,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { } let _ = ApplicationSpecificNotice.setDismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager, values: stickerPackIds).start() }) + } else if groupId == AnyHashable("peerSpecific") { } }, pushController: { [weak controllerInteraction] controller in @@ -2060,19 +2153,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV let inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] _, item, _, _, _ in - guard let strongSelf = self else { - return - } - let hasPremium = strongSelf.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: strongSelf.context.account.peerId)) - |> map { peer -> Bool in - guard case let .user(user) = peer else { - return false - } - return user.isPremium - } - |> distinctUntilChanged - - let _ = (hasPremium |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in + let _ = (ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: nil, premiumIfSavedMessages: false) |> take(1) |> deliverOnMainQueue).start(next: { hasPremium in guard let strongSelf = self else { return } @@ -2168,7 +2249,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV let semaphore = DispatchSemaphore(value: 0) var emojiComponent: EmojiPagerContentComponent? - let _ = ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, areCustomEmojiEnabled: areCustomEmojiEnabled).start(next: { value in + let _ = ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil).start(next: { value in emojiComponent = value semaphore.signal() }) @@ -2183,7 +2264,7 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV gifs: nil, availableGifSearchEmojies: [] ), - updatedInputData: ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, areCustomEmojiEnabled: areCustomEmojiEnabled) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in + updatedInputData: ChatEntityKeyboardInputNode.emojiInputData(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, isStandalone: true, areCustomEmojiEnabled: areCustomEmojiEnabled, chatPeerId: nil) |> map { emojiComponent -> ChatEntityKeyboardInputNode.InputData in return ChatEntityKeyboardInputNode.InputData( emoji: emojiComponent, stickers: nil, diff --git a/submodules/TelegramUI/Sources/ChatInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatInputPanelNode.swift index 659fa7acb2..c93557a86b 100644 --- a/submodules/TelegramUI/Sources/ChatInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatInputPanelNode.swift @@ -7,11 +7,17 @@ import TelegramCore import AccountContext import ChatPresentationInterfaceState +protocol ChatInputPanelViewForOverlayContent: UIView { + func maybeDismissContent(point: CGPoint) +} + class ChatInputPanelNode: ASDisplayNode { var context: AccountContext? var interfaceInteraction: ChatPanelInterfaceInteraction? var prevInputPanelNode: ChatInputPanelNode? + var viewForOverlayContent: ChatInputPanelViewForOverlayContent? + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition) { } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift index ead20ba39f..adcd4b91ed 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContexts.swift @@ -115,14 +115,14 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState) -> return [(NSRange(location: 0, length: inputString.length - (string.count - trimmedString.count)), [.emoji], nil)] } } else { - let activeString = inputText.attributedSubstring(from: NSRange(location: 0, length: inputState.selectionRange.upperBound)) + /*let activeString = inputText.attributedSubstring(from: NSRange(location: 0, length: inputState.selectionRange.upperBound)) if let lastCharacter = activeString.string.last, String(lastCharacter).isSingleEmoji { let matchLength = (String(lastCharacter) as NSString).length if activeString.attribute(ChatTextInputAttributes.customEmoji, at: activeString.length - matchLength, effectiveRange: nil) == nil { return [(NSRange(location: inputState.selectionRange.upperBound - matchLength, length: matchLength), [.emojiSearch], nil)] } - } + }*/ } var possibleTypes = PossibleContextQueryTypes([.command, .mention, .hashtag, .emojiSearch]) diff --git a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift index 790a8655da..65f8555bd5 100644 --- a/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift +++ b/submodules/TelegramUI/Sources/ChatMessageDateHeader.swift @@ -628,14 +628,16 @@ final class ChatMessageAvatarHeaderNode: ListViewItemHeaderNode { videoNode.isUserInteractionEnabled = false videoNode.isHidden = true videoNode.playbackCompleted = { [weak self] in - if let strongSelf = self { - strongSelf.videoLoopCount += 1 - if strongSelf.videoLoopCount == maxVideoLoopCount { - if let videoNode = strongSelf.videoNode { - strongSelf.videoNode = nil - videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in - videoNode?.removeFromSupernode() - }) + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.videoLoopCount += 1 + if strongSelf.videoLoopCount == maxVideoLoopCount { + if let videoNode = strongSelf.videoNode { + strongSelf.videoNode = nil + videoNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak videoNode] _ in + videoNode?.removeFromSupernode() + }) + } } } } diff --git a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift index 0208155705..f3ae68526f 100644 --- a/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatTextInputPanelNode.swift @@ -27,6 +27,8 @@ import EditableChatTextNode import EmojiTextAttachmentView import LottieAnimationComponent import ComponentFlow +import EmojiSuggestionsComponent +import AudioToolbox private let accessoryButtonFont = Font.medium(14.0) private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers]) @@ -417,6 +419,47 @@ enum ChatTextInputPanelPasteData { case sticker(UIImage, Bool) } +final class ChatTextViewForOverlayContent: UIView, ChatInputPanelViewForOverlayContent { + let ignoreHit: (UIView, CGPoint) -> Bool + let dismissSuggestions: () -> Void + + init(ignoreHit: @escaping (UIView, CGPoint) -> Bool, dismissSuggestions: @escaping () -> Void) { + self.ignoreHit = ignoreHit + self.dismissSuggestions = dismissSuggestions + + super.init(frame: CGRect()) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func maybeDismissContent(point: CGPoint) { + for subview in self.subviews.reversed() { + if let _ = subview.hitTest(self.convert(point, to: subview), with: nil) { + return + } + } + + self.dismissSuggestions() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for subview in self.subviews.reversed() { + if let result = subview.hitTest(self.convert(point, to: subview), with: event) { + return result + } + } + + if event == nil || self.ignoreHit(self, point) { + return nil + } + + self.dismissSuggestions() + return nil + } +} + final class CustomEmojiContainerView: UIView { private let emojiViewProvider: (ChatTextInputTextCustomEmojiAttribute) -> UIView? @@ -706,8 +749,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var emojiViewProvider: ((ChatTextInputTextCustomEmojiAttribute) -> UIView)? + private let presentationContext: ChatPresentationContext? + init(context: AccountContext, presentationInterfaceState: ChatPresentationInterfaceState, presentationContext: ChatPresentationContext?, presentController: @escaping (ViewController) -> Void) { self.presentationInterfaceState = presentationInterfaceState + self.presentationContext = presentationContext var hasSpoilers = true if presentationInterfaceState.chatLocation.peerId?.namespace == Namespaces.Peer.SecretChat { @@ -770,6 +816,29 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { super.init() + self.viewForOverlayContent = ChatTextViewForOverlayContent( + ignoreHit: { [weak self] view, point in + guard let strongSelf = self else { + return false + } + if strongSelf.view.hitTest(view.convert(point, to: strongSelf.view), with: nil) != nil { + return true + } + if view.convert(point, to: strongSelf.view).y > strongSelf.view.bounds.maxY { + return true + } + return false + }, + dismissSuggestions: { [weak self] in + guard let strongSelf = self, let currentEmojiSuggestion = strongSelf.currentEmojiSuggestion, let textInputNode = strongSelf.textInputNode else { + return + } + + strongSelf.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position + strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate) + } + ) + self.context = context self.addSubnode(self.clippingNode) @@ -1942,6 +2011,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let textFieldFrame = 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 - textInputViewInternalInsets.bottom)) let shouldUpdateLayout = textFieldFrame.size != textInputNode.frame.size transition.updateFrame(node: textInputNode, frame: textFieldFrame) + self.updateInputField(textInputFrame: textFieldFrame, transition: Transition(transition)) if shouldUpdateLayout { textInputNode.layout() } @@ -2370,6 +2440,201 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } + private struct EmojiSuggestionPosition: Equatable { + var range: NSRange + var value: String + } + + private final class CurrentEmojiSuggestion { + var localPosition: CGPoint + var position: EmojiSuggestionPosition + let disposable: MetaDisposable + var value: [TelegramMediaFile]? + + init(localPosition: CGPoint, position: EmojiSuggestionPosition, disposable: MetaDisposable, value: [TelegramMediaFile]?) { + self.localPosition = localPosition + self.position = position + self.disposable = disposable + self.value = value + } + } + + private var currentEmojiSuggestion: CurrentEmojiSuggestion? + private var currentEmojiSuggestionView: ComponentHostView? + + private var dismissedEmojiSuggestionPosition: EmojiSuggestionPosition? + + private func updateInputField(textInputFrame: CGRect, transition: Transition) { + guard let textInputNode = self.textInputNode, let context = self.context else { + return + } + + var hasTracking = false + var hasTrackingView = false + if textInputNode.selectedRange.length == 0 && textInputNode.selectedRange.location > 0 { + let selectedSubstring = textInputNode.textView.attributedText.attributedSubstring(from: NSRange(location: 0, length: textInputNode.selectedRange.location)) + if let lastCharacter = selectedSubstring.string.last, String(lastCharacter).isSingleEmoji { + let queryLength = (String(lastCharacter) as NSString).length + if selectedSubstring.attribute(ChatTextInputAttributes.customEmoji, at: selectedSubstring.length - queryLength, effectiveRange: nil) == nil { + let beginning = textInputNode.textView.beginningOfDocument + + let characterRange = NSRange(location: selectedSubstring.length - queryLength, length: queryLength) + + let start = textInputNode.textView.position(from: beginning, offset: selectedSubstring.length - queryLength) + let end = textInputNode.textView.position(from: beginning, offset: selectedSubstring.length) + + if let start = start, let end = end, let textRange = textInputNode.textView.textRange(from: start, to: end) { + let selectionRects = textInputNode.textView.selectionRects(for: textRange) + let emojiSuggestionPosition = EmojiSuggestionPosition(range: characterRange, value: String(lastCharacter)) + + hasTracking = true + + if let trackingRect = selectionRects.first?.rect { + let trackingPosition = CGPoint(x: trackingRect.midX, y: trackingRect.minY) + + if self.dismissedEmojiSuggestionPosition == emojiSuggestionPosition { + } else { + hasTrackingView = true + + var beginRequest = false + let suggestionContext: CurrentEmojiSuggestion + if let current = self.currentEmojiSuggestion, current.position.value == emojiSuggestionPosition.value { + suggestionContext = current + } else { + beginRequest = true + suggestionContext = CurrentEmojiSuggestion(localPosition: trackingPosition, position: emojiSuggestionPosition, disposable: MetaDisposable(), value: nil) + self.currentEmojiSuggestion = suggestionContext + } + suggestionContext.localPosition = trackingPosition + self.dismissedEmojiSuggestionPosition = nil + + if beginRequest { + suggestionContext.disposable.set((EmojiSuggestionsComponent.suggestionData(context: context, query: String(lastCharacter)) + |> deliverOnMainQueue).start(next: { [weak self, weak suggestionContext] result in + guard let strongSelf = self, let suggestionContext = suggestionContext, strongSelf.currentEmojiSuggestion === suggestionContext else { + return + } + + suggestionContext.value = result + + if let textInputNode = strongSelf.textInputNode { + strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate) + } + })) + } + } + } + } + } + } + } + + if !hasTracking { + self.dismissedEmojiSuggestionPosition = nil + } + + if let currentEmojiSuggestion = self.currentEmojiSuggestion, let value = currentEmojiSuggestion.value, value.isEmpty { + hasTrackingView = false + } + if !textInputNode.textView.isFirstResponder { + hasTrackingView = false + } + + if !hasTrackingView { + if let currentEmojiSuggestion = self.currentEmojiSuggestion { + self.currentEmojiSuggestion = nil + currentEmojiSuggestion.disposable.dispose() + } + + if let currentEmojiSuggestionView = self.currentEmojiSuggestionView { + self.currentEmojiSuggestionView = nil + + currentEmojiSuggestionView.alpha = 0.0 + currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, completion: { [weak currentEmojiSuggestionView] _ in + currentEmojiSuggestionView?.removeFromSuperview() + }) + } + } + + if let context = self.context, let theme = self.theme, let viewForOverlayContent = self.viewForOverlayContent, let presentationContext = self.presentationContext, let currentEmojiSuggestion = self.currentEmojiSuggestion, let value = currentEmojiSuggestion.value { + let currentEmojiSuggestionView: ComponentHostView + if let current = self.currentEmojiSuggestionView { + currentEmojiSuggestionView = current + } else { + currentEmojiSuggestionView = ComponentHostView() + self.currentEmojiSuggestionView = currentEmojiSuggestionView + viewForOverlayContent.addSubview(currentEmojiSuggestionView) + + currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + + let globalPosition = textInputNode.textView.convert(currentEmojiSuggestion.localPosition, to: self.view) + + let sideInset: CGFloat = 16.0 + + let viewSize = currentEmojiSuggestionView.update( + transition: .immediate, + component: AnyComponent(EmojiSuggestionsComponent( + context: context, + theme: theme, + animationCache: presentationContext.animationCache, + animationRenderer: presentationContext.animationRenderer, + files: value, + action: { [weak self] file in + guard let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction, let currentEmojiSuggestion = strongSelf.currentEmojiSuggestion else { + return + } + + AudioServicesPlaySystemSound(0x450) + + interfaceInteraction.updateTextInputStateAndMode { textInputState, inputMode in + let inputText = NSMutableAttributedString(attributedString: textInputState.inputText) + + var text: String? + var emojiAttribute: ChatTextInputTextCustomEmojiAttribute? + loop: for attribute in file.attributes { + switch attribute { + case let .CustomEmoji(_, displayText, packReference): + text = displayText + emojiAttribute = ChatTextInputTextCustomEmojiAttribute(stickerPack: packReference, fileId: file.fileId.id, file: file) + break loop + default: + break + } + } + + if let emojiAttribute = emojiAttribute, let text = text { + let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]) + + let range = currentEmojiSuggestion.position.range + + inputText.replaceCharacters(in: range, with: replacementText) + let selectionPosition = range.lowerBound + (replacementText.string as NSString).length + + return (ChatTextInputState(inputText: inputText, selectionRange: selectionPosition ..< selectionPosition), inputMode) + } + + return (textInputState, inputMode) + } + + if let textInputNode = strongSelf.textInputNode { + strongSelf.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position + strongSelf.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate) + } + } + )), + environment: {}, + containerSize: CGSize(width: self.bounds.width - sideInset * 2.0, height: 100.0) + ) + + let viewFrame = CGRect(origin: CGPoint(x: max(sideInset, floor(globalPosition.x - viewSize.width / 2.0)), y: globalPosition.y - 2.0 - viewSize.height), size: viewSize) + currentEmojiSuggestionView.frame = viewFrame + if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View { + componentView.adjustBackground(relativePositionX: floor(globalPosition.x - viewFrame.minX)) + } + } + } + private func updateCounterTextNode(transition: ContainedViewLayoutTransition) { if let textInputNode = self.textInputNode, let presentationInterfaceState = self.presentationInterfaceState, let editMessage = presentationInterfaceState.interfaceState.editMessage, let inputTextMaxLength = editMessage.inputTextMaxLength { let textCount = Int32(textInputNode.textView.text.count) @@ -2580,6 +2845,10 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight, metrics: metrics) if !self.bounds.size.height.isEqual(to: panelHeight) { self.updateHeight(animated) + } else { + if let textInputNode = self.textInputNode { + self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate) + } } } } @@ -2644,6 +2913,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { refreshChatTextInputTypingAttributes(textInputNode, theme: presentationInterfaceState.theme, baseFontSize: baseFontSize) self.updateSpoilersRevealed() + + self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate) } } @@ -2671,6 +2942,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { self.storedInputLanguage = editableTextNode.textInputMode.primaryLanguage self.inputMenu.deactivate() + self.dismissedEmojiSuggestionPosition = nil if let presentationInterfaceState = self.presentationInterfaceState { if let peer = presentationInterfaceState.renderedPeer?.peer as? TelegramUser, peer.botInfo != nil, presentationInterfaceState.keyboardButtonsMessage != nil { @@ -3128,6 +3400,15 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return result } } + + if self.bounds.contains(point), let textInputNode = self.textInputNode, let currentEmojiSuggestion = self.currentEmojiSuggestion, let currentEmojiSuggestionView = self.currentEmojiSuggestionView { + if let result = currentEmojiSuggestionView.hitTest(self.view.convert(point, to: currentEmojiSuggestionView), with: event) { + return result + } + self.dismissedEmojiSuggestionPosition = currentEmojiSuggestion.position + self.updateInputField(textInputFrame: textInputNode.frame, transition: .immediate) + } + let result = super.hitTest(point, with: event) return result } diff --git a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift index dafb53d737..c94ca1f172 100644 --- a/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/EmojisChatInputContextPanelNode.swift @@ -20,8 +20,8 @@ private enum EmojisChatInputContextPanelEntryStableId: Hashable, Equatable { } private func backgroundCenterImage(_ theme: PresentationTheme) -> UIImage? { - return generateImage(CGSize(width: 8.0, height: 16.0), rotatedContext: { size, context in - /*context.clear(CGRect(origin: CGPoint(), size: size)) + return generateImage(CGSize(width: 30.0, height: 55.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) context.setStrokeColor(theme.list.itemPlainSeparatorColor.cgColor) context.setFillColor(theme.list.plainBackgroundColor.cgColor) let lineWidth = UIScreenPixel @@ -37,17 +37,8 @@ private func backgroundCenterImage(_ theme: PresentationTheme) -> UIImage? { context.translateBy(x: -460.5, y: -lineWidth / 2.0 - 364.0 + 27.0) context.move(to: CGPoint(x: 0.0, y: lineWidth / 2.0)) context.addLine(to: CGPoint(x: size.width, y: lineWidth / 2.0)) - context.strokePath()*/ - - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setStrokeColor(theme.list.itemPlainSeparatorColor.cgColor) - context.setFillColor(theme.list.plainBackgroundColor.cgColor) - let lineWidth = UIScreenPixel - context.setLineWidth(lineWidth) - - context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.height, height: size.height))) - context.stroke(CGRect(origin: CGPoint(x: -lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.height + lineWidth, height: size.height - lineWidth))) - })?.stretchableImage(withLeftCapWidth: 8, topCapHeight: 8) + context.strokePath() + }) } private func backgroundLeftImage(_ theme: PresentationTheme) -> UIImage? { @@ -201,10 +192,8 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { inner: for (range, type, _) in textInputStateContextQueryRangeAndType(textInputState) { if type == [.emojiSearch] { var range = range - if textInputState.inputText.attributedSubstring(from: range).string.hasPrefix(":") { - range.location -= 1 - range.length += 1 - } + range.location -= 1 + range.length += 1 hashtagQueryRange = range break inner } @@ -278,7 +267,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { self.presentationInterfaceState = interfaceState let sideInsets: CGFloat = 10.0 + leftInset - let contentWidth = min(size.width - sideInsets - sideInsets, max(24.0, CGFloat(self.currentEntries?.count ?? 0) * 45.0 + 5.0)) + let contentWidth = min(size.width - sideInsets - sideInsets, max(24.0, CGFloat(self.currentEntries?.count ?? 0) * 45.0)) var contentLeftInset: CGFloat = 40.0 var leftOffset: CGFloat = 0.0 @@ -290,7 +279,7 @@ final class EmojisChatInputContextPanelNode: ChatInputContextPanelNode { let backgroundFrame = CGRect(origin: CGPoint(x: sideInsets + leftOffset, y: size.height - 55.0 + 4.0), size: CGSize(width: contentWidth, height: 55.0)) let backgroundLeftFrame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: contentLeftInset, height: backgroundFrame.size.height - 10.0 + UIScreenPixel)) - let backgroundCenterFrame = CGRect(origin: CGPoint(x: backgroundLeftFrame.maxX, y: backgroundFrame.minY), size: CGSize(width: 30.0, height: backgroundFrame.size.height - 10.0 + UIScreenPixel)) + let backgroundCenterFrame = CGRect(origin: CGPoint(x: backgroundLeftFrame.maxX, y: backgroundFrame.minY), size: CGSize(width: 30.0, height: 55.0)) let backgroundRightFrame = CGRect(origin: CGPoint(x: backgroundCenterFrame.maxX, y: backgroundFrame.minY), size: CGSize(width: max(0.0, backgroundFrame.minX + backgroundFrame.size.width - backgroundCenterFrame.maxX), height: backgroundFrame.size.height - 10.0 + UIScreenPixel)) transition.updateFrame(node: self.backgroundLeftNode, frame: backgroundLeftFrame) transition.updateFrame(node: self.backgroundNode, frame: backgroundCenterFrame) diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 73efd2e9a8..6fc9b3688e 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -54,8 +54,8 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam } controller.purposefulAction = params.purposefulAction - if params.activateInput { - controller.activateInput() + if let activateInput = params.activateInput { + controller.activateInput(type: activateInput) } if params.changeColors { controller.presentThemeSelection() @@ -138,8 +138,8 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam } } } - if params.activateInput { - controller.activateInput() + if let activateInput = params.activateInput { + controller.activateInput(type: activateInput) } if params.changeColors { Queue.mainQueue().after(0.1) { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 48638afc37..98e118f460 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -7077,7 +7077,6 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate subject: .message(id: .id(index.id), highlight: false, timecode: nil), botStart: nil, updateTextInputState: nil, - activateInput: false, keepStack: .never, useExisting: true, purposefulAction: nil, diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index adedc95265..21d13bf3cb 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -12,6 +12,12 @@ import PhotoResources import UIKitRuntimeUtils import RangeSet +private extension CGRect { + var center: CGPoint { + return CGPoint(x: self.midX, y: self.midY) + } +} + public enum NativeVideoContentId: Hashable { case message(UInt32, MediaId) case instantPage(MediaId, MediaId) diff --git a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift index 8b58713f3a..b54f9eb253 100644 --- a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift +++ b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift @@ -6,6 +6,12 @@ import Display import TelegramPresentationData import TextFormat +private extension CGRect { + var center: CGPoint { + return CGPoint(x: self.midX, y: self.midY) + } +} + private func findScrollView(view: UIView?) -> UIScrollView? { if let view = view { if let view = view as? UIScrollView {