diff --git a/Telegram/BUILD b/Telegram/BUILD index d2666006c3..953b177ea5 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -1958,7 +1958,7 @@ ios_application( }), watch_application = select({ ":disableExtensionsSetting": None, - "//conditions:default": ":TelegramWatchApp", + "//conditions:default": None,#":TelegramWatchApp", }) if telegram_enable_watch else None, deps = [ ":Main", diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 166e9c8659..3cd9dd81a6 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -1537,7 +1537,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else if peer.isFake { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) } else if peer.isVerified { - currentCredibilityIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) + currentCredibilityIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { currentCredibilityIconContent = .premium(color: item.presentationData.theme.list.itemAccentColor) } @@ -1553,7 +1553,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else if peer.isFake { currentCredibilityIconContent = .text(color: item.presentationData.theme.chat.message.incoming.scamColor, string: item.presentationData.strings.Message_FakeAccount.uppercased()) } else if peer.isVerified { - currentCredibilityIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) + currentCredibilityIconContent = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) } else if peer.isPremium && !premiumConfiguration.isPremiumDisabled { currentCredibilityIconContent = .premium(color: item.presentationData.theme.list.itemAccentColor) } diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index cb988e2aea..b7601ab3db 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -280,6 +280,29 @@ public struct Transition { } } + public func setBoundsSize(view: UIView, size: CGSize, completion: ((Bool) -> Void)? = nil) { + if view.bounds.size == size { + completion?(true) + return + } + switch self.animation { + case .none: + view.bounds.size = size + view.layer.removeAnimation(forKey: "bounds.size") + completion?(true) + case .curve: + let previousBounds: CGRect + if view.layer.animation(forKey: "bounds.size") != nil, let presentation = view.layer.presentation() { + previousBounds = presentation.bounds + } else { + previousBounds = view.layer.bounds + } + view.bounds = CGRect(origin: view.bounds.origin, size: size) + + self.animateBoundsSize(view: view, from: previousBounds.size, to: size, completion: completion) + } + } + public func setPosition(view: UIView, position: CGPoint, completion: ((Bool) -> Void)? = nil) { if view.center == position { completion?(true) @@ -552,6 +575,10 @@ public struct Transition { self.animateBoundsOrigin(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion) } + public func animateBoundsSize(view: UIView, from fromValue: CGSize, to toValue: CGSize, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + self.animateBoundsSize(layer: view.layer, from: fromValue, to: toValue, additive: additive, completion: completion) + } + public func animatePosition(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { switch self.animation { case .none: @@ -609,6 +636,25 @@ public struct Transition { } } + public func animateBoundsSize(layer: CALayer, from fromValue: CGSize, to toValue: CGSize, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { + switch self.animation { + case .none: + break + case let .curve(duration, curve): + layer.animate( + from: NSValue(cgSize: fromValue), + to: NSValue(cgSize: toValue), + keyPath: "bounds.size", + duration: duration, + delay: 0.0, + curve: curve, + removeOnCompletion: true, + additive: additive, + completion: completion + ) + } + } + public func setCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) { if layer.cornerRadius == cornerRadius { return diff --git a/submodules/Components/PagerComponent/Sources/PagerComponent.swift b/submodules/Components/PagerComponent/Sources/PagerComponent.swift index f5d31dfa3a..ebe517155a 100644 --- a/submodules/Components/PagerComponent/Sources/PagerComponent.swift +++ b/submodules/Components/PagerComponent/Sources/PagerComponent.swift @@ -44,13 +44,16 @@ public final class PagerComponentChildEnvironment: Equatable { public let containerInsets: UIEdgeInsets public let onChildScrollingUpdate: (ContentScrollingUpdate) -> Void + public let onWantsExclusiveModeUpdated: (Bool) -> Void init( containerInsets: UIEdgeInsets, - onChildScrollingUpdate: @escaping (ContentScrollingUpdate) -> Void + onChildScrollingUpdate: @escaping (ContentScrollingUpdate) -> Void, + onWantsExclusiveModeUpdated: @escaping (Bool) -> Void ) { self.containerInsets = containerInsets self.onChildScrollingUpdate = onChildScrollingUpdate + self.onWantsExclusiveModeUpdated = onWantsExclusiveModeUpdated } public static func ==(lhs: PagerComponentChildEnvironment, rhs: PagerComponentChildEnvironment) -> Bool { @@ -180,6 +183,7 @@ public final class PagerComponent>? public let panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)? public let isTopPanelExpandedUpdated: (Bool, Transition) -> Void + public let isTopPanelHiddenUpdated: (Bool, Transition) -> Void public let panelHideBehavior: PagerComponentPanelHideBehavior public init( @@ -196,6 +200,7 @@ public final class PagerComponent>?, panelStateUpdated: ((PagerComponentPanelState, Transition) -> Void)?, isTopPanelExpandedUpdated: @escaping (Bool, Transition) -> Void, + isTopPanelHiddenUpdated: @escaping (Bool, Transition) -> Void, panelHideBehavior: PagerComponentPanelHideBehavior ) { self.contentInsets = contentInsets @@ -211,6 +216,7 @@ public final class PagerComponent) { self.view = view @@ -431,9 +438,12 @@ public final class PagerComponent Void)?, completion: @escaping () -> Void) { + func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) { if let presentationNode = self.presentationNode { - presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, completion: completion) - return + presentationNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: completion) } - - guard let reactionContextNode = self.reactionContextNode else { - self.animateOut(result: .default, completion: completion) - return - } - var contentCompleted = false - var reactionCompleted = false - let intermediateCompletion: () -> Void = { - if contentCompleted && reactionCompleted { - completion() - } - } - - self.reactionContextNodeIsAnimatingOut = true - reactionContextNode.willAnimateOutToReaction(value: value) - reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, completion: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.reactionContextNode?.removeFromSupernode() - strongSelf.reactionContextNode = nil - reactionCompleted = true - intermediateCompletion() - }) - self.animateOut(result: .default, completion: { - contentCompleted = true - intermediateCompletion() - }) - - self.isUserInteractionEnabled = false } @@ -2521,6 +2490,8 @@ public final class ContextController: ViewController, StandalonePresentableContr private var animatedDidAppear = false private var wasDismissed = false + private var dismissOnInputClose: (result: ContextMenuActionResult, completion: (() -> Void)?)? + private var dismissToReactionOnInputClose: (value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: (() -> Void)?)? override public var overlayWantsToBeBelowKeyboard: Bool { if self.isNodeLoaded { @@ -2669,6 +2640,20 @@ public final class ContextController: ViewController, StandalonePresentableContr super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil) + + if (layout.inputHeight ?? 0.0) == 0.0 { + if let dismissOnInputClose = self.dismissOnInputClose { + self.dismissOnInputClose = nil + DispatchQueue.main.async { + self.dismiss(result: dismissOnInputClose.result, completion: dismissOnInputClose.completion) + } + } else if let args = self.dismissToReactionOnInputClose { + self.dismissToReactionOnInputClose = nil + DispatchQueue.main.async { + self.dismissWithReactionImpl(value: args.value, targetView: args.targetView, hideNode: args.hideNode, animateTargetContainer: args.animateTargetContainer, addStandaloneReactionAnimation: args.addStandaloneReactionAnimation, reducedCurve: true, completion: args.completion) + } + } + } } override public func viewDidAppear(_ animated: Bool) { @@ -2727,8 +2712,15 @@ public final class ContextController: ViewController, StandalonePresentableContr } private func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) { + if viewTreeContainsFirstResponder(view: self.view) { + self.dismissOnInputClose = (result, completion) + self.view.endEditing(true) + return + } + if !self.wasDismissed { self.wasDismissed = true + self.controllerNode.animateOut(result: result, completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) completion?() @@ -2751,9 +2743,19 @@ public final class ContextController: ViewController, StandalonePresentableContr } public func dismissWithReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: (() -> Void)?) { + self.dismissWithReactionImpl(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: false, completion: completion) + } + + private func dismissWithReactionImpl(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: (() -> Void)?) { + if viewTreeContainsFirstResponder(view: self.view) { + self.dismissToReactionOnInputClose = (value, targetView, hideNode, animateTargetContainer, addStandaloneReactionAnimation, completion) + self.view.endEditing(true) + return + } + if !self.wasDismissed { self.wasDismissed = true - self.controllerNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, completion: { [weak self] in + self.controllerNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) completion?() }) diff --git a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift index ef13561317..95211ba751 100644 --- a/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerExtractedPresentationNode.swift @@ -720,7 +720,15 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo reactionContextNodeTransition = .immediate } reactionContextNodeTransition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) - reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: 0.0, right: layout.safeInsets.right), anchorRect: contentRect.offsetBy(dx: contentParentGlobalFrame.minX, dy: 0.0), isAnimatingOut: isAnimatingOut, transition: reactionContextNodeTransition) + + var reactionAnchorRect = contentRect.offsetBy(dx: contentParentGlobalFrame.minX, dy: 0.0) + + let bottomInset = layout.insets(options: [.input]).bottom + if reactionAnchorRect.minY > layout.size.height - bottomInset { + reactionAnchorRect.origin.y = layout.size.height - bottomInset + } + + reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: 0.0, right: layout.safeInsets.right), anchorRect: reactionAnchorRect, isAnimatingOut: isAnimatingOut, transition: reactionContextNodeTransition) self.proposedReactionsPositionLock = contentRect.minY - 18.0 - reactionContextNode.contentHeight - 46.0 } else { @@ -1145,7 +1153,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo } } - func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { + func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) { guard let reactionContextNode = self.reactionContextNode else { self.requestAnimateOut(.default, completion) return @@ -1162,7 +1170,14 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo self.reactionContextNodeIsAnimatingOut = true reactionContextNode.willAnimateOutToReaction(value: value) - self.requestAnimateOut(.default, { + let result: ContextMenuActionResult + if reducedCurve { + result = .custom(.animated(duration: 0.5, curve: .spring)) + } else { + result = .default + } + + self.requestAnimateOut(result, { contentCompleted = true intermediateCompletion() }) diff --git a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift index 1fb23869ff..caf6c57fbe 100644 --- a/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerPresentationNode.swift @@ -26,7 +26,7 @@ protocol ContextControllerPresentationNode: ASDisplayNode { stateTransition: ContextControllerPresentationNodeStateTransition? ) - func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) + func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) func cancelReactionAnimation() func highlightGestureMoved(location: CGPoint, hover: Bool) diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index df1ff367c5..b5b58f0352 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -408,6 +408,7 @@ open class NavigationController: UINavigationController, ContainableController, } else { if let statusBarHost = self.statusBarHost, let keyboardWindow = statusBarHost.keyboardWindow, let keyboardView = statusBarHost.keyboardView, !keyboardView.frame.height.isZero, isViewVisibleInHierarchy(keyboardView) { if globalOverlayContainerParent.view.superview != keyboardWindow { + globalOverlayContainerParent.layer.zPosition = 1000.0 keyboardWindow.addSubnode(globalOverlayContainerParent) } } else if globalOverlayContainerParent.view.superview !== self.displayNode.view { diff --git a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift index 6d07e6d777..878556c6de 100644 --- a/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift +++ b/submodules/ItemListPeerItem/Sources/ItemListPeerItem.swift @@ -648,7 +648,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo } else if case let .user(user) = item.peer, let emojiStatus = user.emojiStatus { credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if item.peer.isVerified { - credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) + credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) } else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled { credibilityIcon = .premium(color: item.presentationData.theme.list.itemAccentColor) } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index cfde70badf..a2d17d03b0 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -234,6 +234,9 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private var emojiContentDisposable: Disposable? + private let emojiSearchDisposable = MetaDisposable() + private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil) + private var horizontalExpandRecognizer: UIPanGestureRecognizer? private var horizontalExpandStartLocation: CGPoint? private var horizontalExpandDistance: CGFloat = 0.0 @@ -249,6 +252,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var genericReactionEffectDisposable: Disposable? private var genericReactionEffect: String? + private var isReactionSearchActive: Bool = false + public static func randomGenericReactionEffect(context: AccountContext) -> Signal { return context.engine.stickers.loadedStickerPack(reference: .emojiGenericAnimations, forceActualized: false) |> map { result -> [TelegramMediaFile]? in @@ -404,12 +409,19 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { }) if let getEmojiContent = getEmojiContent { - self.emojiContentDisposable = (getEmojiContent(self.animationCache, self.animationRenderer) - |> deliverOnMainQueue).start(next: { [weak self] emojiContent in + self.emojiContentDisposable = combineLatest(queue: .mainQueue(), + getEmojiContent(self.animationCache, self.animationRenderer), + self.emojiSearchResult.get() + ).start(next: { [weak self] emojiContent, emojiSearchResult in guard let strongSelf = self else { return } + var emojiContent = emojiContent + if let emojiSearchResult = emojiSearchResult { + emojiContent = emojiContent.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id) + } + strongSelf.emojiContent = emojiContent if !strongSelf.canBeExpanded { strongSelf.canBeExpanded = true @@ -436,7 +448,15 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { deviceMetrics: DeviceMetrics.iPhone13, emojiContent: emojiContent, backgroundColor: .clear, - separatorColor: strongSelf.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5) + separatorColor: strongSelf.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5), + hideTopPanel: strongSelf.isReactionSearchActive, + hideTopPanelUpdated: { hideTopPanel, transition in + guard let strongSelf = self else { + return + } + strongSelf.isReactionSearchActive = hideTopPanel + strongSelf.requestLayout(transition.containedViewLayoutTransition) + } )), environment: {}, containerSize: CGSize(width: componentView.bounds.width, height: 300.0) @@ -456,6 +476,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.availableReactionsDisposable?.dispose() self.hasPremiumDisposable?.dispose() self.genericReactionEffectDisposable?.dispose() + self.emojiSearchDisposable.dispose() } override public func didLoad() { @@ -830,7 +851,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { expandItemSize = 30.0 expandTintOffset = 0.0 } - let baseNextFrame = CGRect(origin: CGPoint(x: self.scrollNode.view.bounds.width - expandItemSize - 9.0, y: containerHeight - contentHeight + floor((contentHeight - expandItemSize) / 2.0) + (self.isExpanded ? (46.0 + 54.0 - 4.0) : 0.0)), size: CGSize(width: expandItemSize, height: expandItemSize + self.extensionDistance)) + let baseNextFrame = CGRect(origin: CGPoint(x: self.scrollNode.view.bounds.width - expandItemSize - 9.0, y: containerHeight - contentHeight + floor((contentHeight - expandItemSize) / 2.0) + (self.isExpanded ? (46.0) : 0.0)), size: CGSize(width: expandItemSize, height: expandItemSize + self.extensionDistance)) transition.updateFrame(view: expandItemView, frame: baseNextFrame) transition.updateFrame(view: expandItemView.tintView, frame: baseNextFrame.offsetBy(dx: 0.0, dy: expandTintOffset)) @@ -931,7 +952,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { visibleItemCount: itemCount ) - var scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: self.isExpanded ? (46.0 + 54.0 - 4.0) : 0.0), size: actualBackgroundFrame.size) + var scrollFrame = CGRect(origin: CGPoint(x: 0.0, y: self.isExpanded ? (46.0) : 0.0), size: actualBackgroundFrame.size) scrollFrame.origin.y += floorToScreenPixels(self.extensionDistance / 2.0) transition.updateFrame(node: self.contentContainer, frame: visualBackgroundFrame, beginWithCurrentState: true) @@ -978,7 +999,15 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { deviceMetrics: DeviceMetrics.iPhone13, emojiContent: emojiContent, backgroundColor: .clear, - separatorColor: self.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5) + separatorColor: self.presentationData.theme.list.itemPlainSeparatorColor.withMultipliedAlpha(0.5), + hideTopPanel: self.isReactionSearchActive, + hideTopPanelUpdated: { [weak self] hideTopPanel, transition in + guard let strongSelf = self else { + return + } + strongSelf.isReactionSearchActive = hideTopPanel + strongSelf.requestLayout(transition.containedViewLayoutTransition) + } )), environment: {}, containerSize: CGSize(width: actualBackgroundFrame.width, height: 300.0) @@ -1023,7 +1052,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if let mirrorContentClippingView = emojiView.mirrorContentClippingView { mirrorContentClippingView.clipsToBounds = false - Transition(transition).animateBoundsOrigin(view: mirrorContentClippingView, from: CGPoint(x: 0.0, y: 46.0 + 54.0 - 4.0), to: CGPoint(), additive: true, completion: { [weak mirrorContentClippingView] _ in + Transition(transition).animateBoundsOrigin(view: mirrorContentClippingView, from: CGPoint(x: 0.0, y: 46.0), to: CGPoint(), additive: true, completion: { [weak mirrorContentClippingView] _ in mirrorContentClippingView?.clipsToBounds = true }) } @@ -1053,7 +1082,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { componentTransition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height))) if animateIn { - transition.animatePositionAdditive(layer: componentView.layer, offset: CGPoint(x: 0.0, y: -(46.0 + 54.0 - 4.0) + floorToScreenPixels(self.animateFromExtensionDistance / 2.0))) + transition.animatePositionAdditive(layer: componentView.layer, offset: CGPoint(x: 0.0, y: -(46.0) + floorToScreenPixels(self.animateFromExtensionDistance / 2.0))) } } } @@ -1247,7 +1276,116 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } strongSelf.requestUpdateOverlayWantsToBeBelowKeyboard(transition.containedViewLayoutTransition) }, - sendSticker: nil, + updateSearchQuery: { [weak self] query in + guard let strongSelf = self else { + return + } + + if query.isEmpty { + strongSelf.emojiSearchDisposable.set(nil) + strongSelf.emojiSearchResult.set(.single(nil)) + } else { + let context = strongSelf.context + + let languageCode = "en" + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: query.count < 2) + if !languageCode.lowercased().hasPrefix("en") { + signal = signal + |> mapToSignal { keywords in + return .single(keywords) + |> then( + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3) + |> map { englishKeywords in + return keywords + englishKeywords + } + ) + } + } + + 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 resultSignal = signal + |> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + return combineLatest( + context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), + hasPremium + ) + |> map { view, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in + var result: [(String, TelegramMediaFile?, String)] = [] + + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + } + } + + for entry in view.entries { + guard let item = entry.item as? StickerPackItem else { + continue + } + for attribute in item.file.attributes { + switch attribute { + case let .CustomEmoji(_, alt, _): + if !alt.isEmpty, let keyword = allEmoticons[alt] { + if !item.file.isPremiumEmoji || hasPremium { + result.append((alt, item.file, keyword)) + } + } + default: + break + } + } + } + + for keyword in keywords { + for emoticon in keyword.emoticons { + result.append((emoticon, nil, keyword.keyword)) + } + } + + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for item in result { + if let itemFile = item.1 { + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, subgroupId: nil, + icon: .none, + accentTint: false + ) + items.append(item) + } + } + + return [EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", groupId: "search", title: nil, subtitle: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, items: items)] + } + } + + strongSelf.emojiSearchDisposable.set((resultSignal + |> deliverOnMainQueue).start(next: { result in + guard let strongSelf = self else { + return + } + strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query)))) + })) + } + }, chatPeerId: nil, peekBehavior: nil, customLayout: emojiContentLayout, diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift index b94446344a..9f4b45f602 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatParticipantItem.swift @@ -843,7 +843,7 @@ class VoiceChatParticipantItemNode: ItemListRevealOptionsItemNode { } else if let user = item.peer as? TelegramUser, let emojiStatus = user.emojiStatus { credibilityIcon = .animation(content: .customEmoji(fileId: emojiStatus.fileId), size: CGSize(width: 20.0, height: 20.0), placeholderColor: item.presentationData.theme.list.mediaPlaceholderColor, themeColor: item.presentationData.theme.list.itemAccentColor, loopMode: .count(2)) } else if item.peer.isVerified { - credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor) + credibilityIcon = .verified(fillColor: item.presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .compact) } else if item.peer.isPremium && !premiumConfiguration.isPremiumDisabled { credibilityIcon = .premium(color: item.presentationData.theme.list.itemAccentColor) } diff --git a/submodules/TelegramCore/Sources/LoadedPeerFromMessage.swift b/submodules/TelegramCore/Sources/LoadedPeerFromMessage.swift index a2e5f42fc8..396b3fc746 100644 --- a/submodules/TelegramCore/Sources/LoadedPeerFromMessage.swift +++ b/submodules/TelegramCore/Sources/LoadedPeerFromMessage.swift @@ -47,9 +47,7 @@ public func loadedPeerFromMessage(account: Account, peerId: PeerId, messageId: M for user in apiUsers { let telegramUser = TelegramUser(user: user) if telegramUser.id == peerId, let accessHash = telegramUser.accessHash, accessHash.value != 0 { - if let presence = TelegramUserPresence(apiUser: user) { - updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: [telegramUser.id: presence]) - } + updatePeerPresences(transaction: transaction, accountPeerId: account.peerId, peerPresences: [telegramUser.id: user]) updatePeers(transaction: transaction, peers: [telegramUser], update: { _, updated -> Peer in return updated diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index a83acc73b0..20cbc64332 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -3170,7 +3170,7 @@ func replayFinalState( } } - updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: presences) + updatePeerPresencesClean(transaction: transaction, accountPeerId: accountPeerId, peerPresences: presences) case let .UpdateSecretChat(chat, _): updateSecretChat(encryptionProvider: encryptionProvider, accountPeerId: accountPeerId, transaction: transaction, mediaBox: mediaBox, chat: chat, requestData: nil) case let .AddSecretMessages(messages): diff --git a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift index 3b3c26f7c5..2612a775a3 100644 --- a/submodules/TelegramCore/Sources/State/AccountViewTracker.swift +++ b/submodules/TelegramCore/Sources/State/AccountViewTracker.swift @@ -105,7 +105,7 @@ private func fetchWebpage(account: Account, messageId: MessageId) -> Signal Void in var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = mergeGroupOrChannel(lhs: transaction.getPeer(chat.peerId), rhs: chat) { peers.append(groupOrChannel) @@ -114,9 +114,7 @@ private func fetchWebpage(account: Account, messageId: MessageId) -> Signal [MessageId: ViewCountContextState] in var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] var resultStates: [MessageId: ViewCountContextState] = [:] for apiUser in users { if let user = TelegramUser.merge(transaction.getPeer(apiUser.peerId) as? TelegramUser, rhs: apiUser) { peers.append(user) - if let presence = TelegramUserPresence(apiUser: apiUser) { - peerPresences[user.id] = presence - } + peerPresences[user.id] = apiUser } } for chat in chats { @@ -1086,7 +1082,7 @@ public final class AccountViewTracker { |> mapToSignal { messages, chats, users -> Signal in return account.postbox.transaction { transaction -> Void in var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = mergeGroupOrChannel(lhs: transaction.getPeer(chat.peerId), rhs: chat) { @@ -1096,9 +1092,7 @@ public final class AccountViewTracker { for apiUser in users { if let user = TelegramUser.merge(transaction.getPeer(apiUser.peerId) as? TelegramUser, rhs: apiUser) { peers.append(user) - if let presence = TelegramUserPresence(apiUser: apiUser) { - peerPresences[user.id] = presence - } + peerPresences[user.id] = apiUser } } diff --git a/submodules/TelegramCore/Sources/State/ContactSyncManager.swift b/submodules/TelegramCore/Sources/State/ContactSyncManager.swift index f4388c6d3c..e55613f000 100644 --- a/submodules/TelegramCore/Sources/State/ContactSyncManager.swift +++ b/submodules/TelegramCore/Sources/State/ContactSyncManager.swift @@ -400,7 +400,7 @@ private func updateContactPresences(postbox: Postbox, network: Network, accountP peerPresences[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))] = TelegramUserPresence(apiStatus: status) } } - updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) + updatePeerPresencesClean(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) } |> ignoreValues } diff --git a/submodules/TelegramCore/Sources/State/FetchChatList.swift b/submodules/TelegramCore/Sources/State/FetchChatList.swift index ff4a1dd8f3..ccf46bec66 100644 --- a/submodules/TelegramCore/Sources/State/FetchChatList.swift +++ b/submodules/TelegramCore/Sources/State/FetchChatList.swift @@ -13,7 +13,7 @@ enum FetchChatListLocation { struct ParsedDialogs { let itemIds: [PeerId] let peers: [Peer] - let peerPresences: [PeerId: PeerPresence] + let peerPresences: [PeerId: Api.User] let notificationSettings: [PeerId: PeerNotificationSettings] let readStates: [PeerId: [MessageId.Namespace: PeerReadState]] @@ -61,7 +61,7 @@ private func parseDialogs(apiDialogs: [Api.Dialog], apiMessages: [Api.Message], var itemIds: [PeerId] = [] var peers: [PeerId: Peer] = [:] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in apiChats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { peers[groupOrChannel.id] = groupOrChannel @@ -70,9 +70,7 @@ private func parseDialogs(apiDialogs: [Api.Dialog], apiMessages: [Api.Message], for user in apiUsers { let telegramUser = TelegramUser(user: user) peers[telegramUser.id] = telegramUser - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } for dialog in apiDialogs { @@ -191,7 +189,7 @@ private func parseDialogs(apiDialogs: [Api.Dialog], apiMessages: [Api.Message], struct FetchedChatList { let chatPeerIds: [PeerId] let peers: [Peer] - let peerPresences: [PeerId: PeerPresence] + let peerPresences: [PeerId: Api.User] let notificationSettings: [PeerId: PeerNotificationSettings] let readStates: [PeerId: [MessageId.Namespace: PeerReadState]] let mentionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary] @@ -297,7 +295,7 @@ func fetchChatList(postbox: Postbox, network: Network, location: FetchChatListLo return combineLatest(folderSignals) |> mapToSignal { folders -> Signal in var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] var notificationSettings: [PeerId: PeerNotificationSettings] = [:] var readStates: [PeerId: [MessageId.Namespace: PeerReadState]] = [:] var mentionTagSummaries: [PeerId: MessageHistoryTagNamespaceSummary] = [:] diff --git a/submodules/TelegramCore/Sources/State/Holes.swift b/submodules/TelegramCore/Sources/State/Holes.swift index 97f409dc68..a5f80f53be 100644 --- a/submodules/TelegramCore/Sources/State/Holes.swift +++ b/submodules/TelegramCore/Sources/State/Holes.swift @@ -566,7 +566,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH } var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { peers.append(groupOrChannel) @@ -575,9 +575,7 @@ func fetchMessageHistoryHole(accountPeerId: PeerId, source: FetchMessageHistoryH for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } var storeMessages: [StoreMessage] = [] @@ -838,7 +836,7 @@ func fetchCallListHole(network: Network, postbox: Postbox, accountPeerId: PeerId transaction.replaceGlobalMessageTagsHole(globalTags: [.Calls, .MissedCalls], index: holeIndex, with: updatedIndex, messages: storeMessages) var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { peers.append(groupOrChannel) @@ -847,9 +845,7 @@ func fetchCallListHole(network: Network, postbox: Postbox, accountPeerId: PeerId for user in users { if let telegramUser = TelegramUser.merge(transaction.getPeer(user.peerId) as? TelegramUser, rhs: user) { peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } } diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift index ef5528b424..cb0f03c24e 100644 --- a/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizePinnedChatsOperations.swift @@ -136,7 +136,7 @@ private func synchronizePinnedChats(transaction: Transaction, postbox: Postbox, var remoteItemIds: [PinnedItemId] = [] var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] switch dialogs { case let .peerDialogs(dialogs, messages, chats, users, _): @@ -148,9 +148,7 @@ private func synchronizePinnedChats(transaction: Transaction, postbox: Postbox, for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } loop: for dialog in dialogs { diff --git a/submodules/TelegramCore/Sources/State/MessageReactions.swift b/submodules/TelegramCore/Sources/State/MessageReactions.swift index a12cb615ab..16c672837e 100644 --- a/submodules/TelegramCore/Sources/State/MessageReactions.swift +++ b/submodules/TelegramCore/Sources/State/MessageReactions.swift @@ -480,14 +480,12 @@ public final class EngineMessageReactionListContext { switch result { case let .messageReactionsList(_, count, reactions, chats, users, nextOffset): var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } for chat in chats { if let peer = parseTelegramGroupOrChannel(chat: chat) { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift index 7453cb4b8e..c0f5431130 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Calls/GroupCalls.swift @@ -97,14 +97,12 @@ func _internal_getCurrentGroupCall(account: Account, callId: Int64, accessHash: } var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } for chat in chats { @@ -383,14 +381,12 @@ func _internal_getGroupCallParticipants(account: Account, callId: Int64, accessH } var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } for chat in chats { @@ -596,14 +592,12 @@ func _internal_joinGroupCall(account: Account, peerId: PeerId, joinAs: PeerId?, state.adminIds = Set(peerAdminIds) var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for user in apiUsers { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } let connectionMode: JoinGroupCallResult.ConnectionMode diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift index 98a2b5f991..17d6c2e06f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift @@ -73,7 +73,7 @@ func syncContactsOnce(network: Network, postbox: Postbox, accountPeerId: PeerId) transaction.replaceRemoteContactCount(totalCount) - updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) + updatePeerPresencesClean(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences) if wasEmpty { var insertSignal: Signal = .complete() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift index c602bd5719..3ab8de154a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/AdMessages.swift @@ -400,7 +400,7 @@ private class AdMessagesHistoryContextImpl { switch result { case let .sponsoredMessages(messages, chats, users): var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { @@ -410,9 +410,7 @@ private class AdMessagesHistoryContextImpl { for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift index 131cdef8bb..f5ec6920b2 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ClearCloudDrafts.swift @@ -13,7 +13,7 @@ func _internal_clearCloudDraftsInteractively(postbox: Postbox, network: Network, switch updates { case let .updates(updates, users, chats, _, _): var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { peers.append(groupOrChannel) @@ -22,9 +22,7 @@ func _internal_clearCloudDraftsInteractively(postbox: Postbox, network: Network, for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } for update in updates { switch update { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift index 052613f22c..f28300ed05 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/LoadMessagesIfNecessary.swift @@ -94,7 +94,7 @@ func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Po } var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { peers.append(groupOrChannel) @@ -103,9 +103,7 @@ func _internal_getMessagesLoadIfNecessary(_ messageIds: [MessageId], postbox: Po for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } updatePeers(transaction: transaction, peers: peers, update: { _, updated -> Peer in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift index b2ccc018eb..f11c6beddf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/ReplyThreadHistory.swift @@ -171,7 +171,7 @@ private class ReplyThreadHistoryContextImpl { } var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { @@ -181,9 +181,7 @@ private class ReplyThreadHistoryContextImpl { for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } let _ = transaction.addMessages(parsedMessages, location: .Random) @@ -608,7 +606,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa } var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { @@ -618,9 +616,7 @@ func _internal_fetchChannelReplyThreadMessage(account: Account, messageId: Messa for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } let _ = transaction.addMessages(parsedMessages, location: .Random) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift index 66a1a5f027..643d238f1f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/SparseMessageList.swift @@ -925,7 +925,7 @@ public final class SparseMessageCalendar { case let .searchResultsCalendar(_, _, minDate, minMsgId, _, periods, messages, chats, users): var parsedMessages: [StoreMessage] = [] var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { @@ -935,9 +935,7 @@ public final class SparseMessageCalendar { for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } for message in messages { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift index 6d55b2754b..ca8c3297a3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift @@ -91,13 +91,11 @@ func _internal_channelMembers(postbox: Postbox, network: Network, accountPeerId: switch result { case let .channelParticipants(_, participants, chats, users): var peers: [PeerId: Peer] = [:] - var presences: [PeerId: PeerPresence] = [:] + var presences: [PeerId: Api.User] = [:] for user in users { let peer = TelegramUser(user: user) peers[peer.id] = peer - if let presence = TelegramUserPresence(apiUser: user) { - presences[peer.id] = presence - } + presences[peer.id] = user } for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { @@ -111,7 +109,11 @@ func _internal_channelMembers(postbox: Postbox, network: Network, accountPeerId: for participant in CachedChannelParticipants(apiParticipants: participants).participants { if let peer = peers[participant.peerId] { - items.append(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: presences)) + var renderedPresences: [PeerId: PeerPresence] = [:] + if let presence = transaction.getPeerPresence(peerId: participant.peerId) { + renderedPresences[participant.peerId] = presence + } + items.append(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: renderedPresences)) } } case .channelParticipantsNotModified: diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift index cdd0e71930..109ab45420 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ChatListFiltering.swift @@ -633,7 +633,7 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox, return postbox.transaction { transaction -> Void in var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] var notificationSettings: [PeerId: PeerNotificationSettings] = [:] var channelStates: [PeerId: Int32] = [:] @@ -647,9 +647,7 @@ private func loadAndStorePeerChatInfos(accountPeerId: PeerId, postbox: Postbox, for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } var topMessageIds = Set() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift index edb1be713d..b298bc0f0f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/RecentPeers.swift @@ -66,13 +66,11 @@ func _internal_managedUpdatedRecentPeers(accountPeerId: PeerId, postbox: Postbox switch result { case let .topPeers(_, _, users): var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } updatePeers(transaction: transaction, peers: peers, update: { return $1 }) @@ -158,19 +156,17 @@ func _internal_updateRecentPeersEnabled(postbox: Postbox, network: Network, enab func _internal_managedRecentlyUsedInlineBots(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal { let remotePeers = network.request(Api.functions.contacts.getTopPeers(flags: 1 << 2, offset: 0, limit: 16, hash: 0)) |> retryRequest - |> map { result -> ([Peer], [PeerId: PeerPresence], [(PeerId, Double)])? in + |> map { result -> ([Peer], [PeerId: Api.User], [(PeerId, Double)])? in switch result { case .topPeersDisabled: break case let .topPeers(categories, _, users): var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } var peersWithRating: [(PeerId, Double)] = [] for category in categories { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift index 7cfe3db726..9150d78dfb 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/UpdateCachedPeerData.swift @@ -79,7 +79,7 @@ func fetchAndUpdateSupplementalCachedPeerData(peerId rawPeerId: PeerId, accountP |> retryRequest |> mapToSignal { peerSettings -> Signal in var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] let peerStatusSettings: PeerStatusSettings switch peerSettings { @@ -93,9 +93,7 @@ func fetchAndUpdateSupplementalCachedPeerData(peerId rawPeerId: PeerId, accountP for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } } @@ -198,7 +196,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee case let .userFull(fullUser, chats, users): var accountUser: Api.User? var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let peer = parseTelegramGroupOrChannel(chat: chat) { peers.append(peer) @@ -207,9 +205,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee for user in users { let telegramUser = TelegramUser(user: user) peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user if telegramUser.id == accountPeerId { accountUser = user } @@ -327,7 +323,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee let pinnedMessageId = chatFullPinnedMsgId.flatMap({ MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }) var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { peers.append(groupOrChannel) @@ -336,9 +332,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee for user in users { if let telegramUser = TelegramUser.merge(transaction.getPeer(user.peerId) as? TelegramUser, rhs: user) { peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } } @@ -515,7 +509,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee } var peers: [Peer] = [] - var peerPresences: [PeerId: PeerPresence] = [:] + var peerPresences: [PeerId: Api.User] = [:] for chat in chats { if let groupOrChannel = parseTelegramGroupOrChannel(chat: chat) { peers.append(groupOrChannel) @@ -524,9 +518,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee for user in users { if let telegramUser = TelegramUser.merge(transaction.getPeer(user.peerId) as? TelegramUser, rhs: user) { peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } } @@ -536,9 +528,7 @@ func _internal_fetchAndUpdateCachedPeerData(accountPeerId: PeerId, peerId rawPee for user in users { if let telegramUser = TelegramUser.merge(transaction.getPeer(user.peerId) as? TelegramUser, rhs: user) { peers.append(telegramUser) - if let presence = TelegramUserPresence(apiUser: user) { - peerPresences[telegramUser.id] = presence - } + peerPresences[telegramUser.id] = user } } for chat in chats { diff --git a/submodules/TelegramCore/Sources/UpdatePeers.swift b/submodules/TelegramCore/Sources/UpdatePeers.swift index 66cafb5bcb..ed09bec470 100644 --- a/submodules/TelegramCore/Sources/UpdatePeers.swift +++ b/submodules/TelegramCore/Sources/UpdatePeers.swift @@ -106,7 +106,40 @@ public func updatePeers(transaction: Transaction, peers: [Peer], update: (Peer?, }) } -func updatePeerPresences(transaction: Transaction, accountPeerId: PeerId, peerPresences: [PeerId: PeerPresence]) { +func updatePeerPresences(transaction: Transaction, accountPeerId: PeerId, peerPresences: [PeerId: Api.User]) { + var parsedPresences: [PeerId: PeerPresence] = [:] + for (peerId, user) in peerPresences { + guard let presence = TelegramUserPresence(apiUser: user) else { + continue + } + switch presence.status { + case .present: + parsedPresences[peerId] = presence + default: + switch user { + case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _): + let isMin = (flags & (1 << 20)) != 0 + if isMin, let _ = transaction.getPeerPresence(peerId: peerId) { + } else { + parsedPresences[peerId] = presence + } + default: + break + } + } + } + + parsedPresences.removeValue(forKey: accountPeerId) + + transaction.updatePeerPresencesInternal(presences: parsedPresences, merge: { previous, updated in + if let previous = previous as? TelegramUserPresence, let updated = updated as? TelegramUserPresence, previous.lastActivity != updated.lastActivity { + return TelegramUserPresence(status: updated.status, lastActivity: max(previous.lastActivity, updated.lastActivity)) + } + return updated + }) +} + +func updatePeerPresencesClean(transaction: Transaction, accountPeerId: PeerId, peerPresences: [PeerId: PeerPresence]) { var peerPresences = peerPresences if peerPresences[accountPeerId] != nil { peerPresences.removeValue(forKey: accountPeerId) diff --git a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift index 0760f120df..1716525d34 100644 --- a/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusComponent/Sources/EmojiStatusComponent.swift @@ -37,10 +37,15 @@ public final class EmojiStatusComponent: Component { case count(Int) } + public enum SizeType { + case compact + case large + } + public enum Content: Equatable { case none case premium(color: UIColor) - case verified(fillColor: UIColor, foregroundColor: UIColor) + case verified(fillColor: UIColor, foregroundColor: UIColor, sizeType: SizeType) case text(color: UIColor, string: String) case animation(content: AnimationContent, size: CGSize, placeholderColor: UIColor, themeColor: UIColor?, loopMode: LoopMode) } @@ -217,8 +222,16 @@ public final class EmojiStatusComponent: Component { } else { iconImage = nil } - case let .verified(fillColor, foregroundColor): - if let backgroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconBackground"), let foregroundImage = UIImage(bundleImageName: "Peer Info/VerifiedIconForeground") { + case let .verified(fillColor, foregroundColor, sizeType): + let imageNamePrefix: String + switch sizeType { + case .compact: + imageNamePrefix = "Chat List/PeerVerifiedIcon" + case .large: + imageNamePrefix = "Peer Info/VerifiedIcon" + } + + if let backgroundImage = UIImage(bundleImageName: "\(imageNamePrefix)Background"), let foregroundImage = UIImage(bundleImageName: "\(imageNamePrefix)Foreground") { iconImage = generateImage(backgroundImage.size, contextGenerator: { size, context in if let backgroundCgImage = backgroundImage.cgImage, let foregroundCgImage = foregroundImage.cgImage { context.clear(CGRect(origin: CGPoint(), size: size)) @@ -342,7 +355,16 @@ public final class EmojiStatusComponent: Component { } iconView.image = iconImage - if case .text = component.content { + var useFit = false + switch component.content { + case .text: + useFit = true + case .verified(_, _, sizeType: .compact): + useFit = true + default: + break + } + if useFit { size = CGSize(width: iconImage.size.width, height: availableSize.height) iconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconImage.size.width) / 2.0), y: floor((size.height - iconImage.size.height) / 2.0)), size: iconImage.size) } else { diff --git a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift index f7d1fcdaaf..21ca0a345c 100644 --- a/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift +++ b/submodules/TelegramUI/Components/EmojiStatusSelectionComponent/Sources/EmojiStatusSelectionComponent.swift @@ -64,6 +64,8 @@ public final class EmojiStatusSelectionComponent: Component { public let emojiContent: EmojiPagerContentComponent public let backgroundColor: UIColor public let separatorColor: UIColor + public let hideTopPanel: Bool + public let hideTopPanelUpdated: (Bool, Transition) -> Void public init( theme: PresentationTheme, @@ -71,7 +73,9 @@ public final class EmojiStatusSelectionComponent: Component { deviceMetrics: DeviceMetrics, emojiContent: EmojiPagerContentComponent, backgroundColor: UIColor, - separatorColor: UIColor + separatorColor: UIColor, + hideTopPanel: Bool, + hideTopPanelUpdated: @escaping (Bool, Transition) -> Void ) { self.theme = theme self.strings = strings @@ -79,6 +83,8 @@ public final class EmojiStatusSelectionComponent: Component { self.emojiContent = emojiContent self.backgroundColor = backgroundColor self.separatorColor = separatorColor + self.hideTopPanel = hideTopPanel + self.hideTopPanelUpdated = hideTopPanelUpdated } public static func ==(lhs: EmojiStatusSelectionComponent, rhs: EmojiStatusSelectionComponent) -> Bool { @@ -100,6 +106,9 @@ public final class EmojiStatusSelectionComponent: Component { if lhs.separatorColor != rhs.separatorColor { return false } + if lhs.hideTopPanel != rhs.hideTopPanel { + return false + } return true } @@ -111,6 +120,7 @@ public final class EmojiStatusSelectionComponent: Component { private let panelSeparatorView: UIView private var component: EmojiStatusSelectionComponent? + private weak var state: EmptyComponentState? override init(frame: CGRect) { self.keyboardView = ComponentView() @@ -131,6 +141,9 @@ public final class EmojiStatusSelectionComponent: Component { fatalError("init(coder:) has not been implemented") } + deinit { + } + func update(component: EmojiStatusSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.backgroundColor = component.backgroundColor let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) @@ -138,8 +151,9 @@ public final class EmojiStatusSelectionComponent: Component { self.panelSeparatorView.backgroundColor = component.separatorColor self.component = component + self.state = state - let topPanelHeight: CGFloat = 42.0 + let topPanelHeight: CGFloat = component.hideTopPanel ? 0.0 : 42.0 let keyboardSize = self.keyboardView.update( transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), @@ -158,6 +172,12 @@ public final class EmojiStatusSelectionComponent: Component { externalTopPanelContainer: self.panelHostView, topPanelExtensionUpdated: { _, _ in }, hideInputUpdated: { _, _, _ in }, + hideTopPanelUpdated: { [weak self] hideTopPanel, transition in + guard let strongSelf = self else { + return + } + strongSelf.component?.hideTopPanelUpdated(hideTopPanel, transition) + }, switchToTextInput: {}, switchToGifSubject: { _ in }, reorderItems: { _, _ in }, @@ -189,7 +209,7 @@ public final class EmojiStatusSelectionComponent: Component { transition.setFrame(view: self.panelBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: keyboardSize.width, height: topPanelHeight))) self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) - transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: keyboardSize.width, height: UIScreenPixel))) + transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: component.hideTopPanel ? -UIScreenPixel : topPanelHeight), size: CGSize(width: keyboardSize.width, height: UIScreenPixel))) } return availableSize @@ -343,7 +363,8 @@ public final class EmojiStatusSelectionController: ViewController { }, requestUpdate: { _ in }, - sendSticker: nil, + updateSearchQuery: { _ in + }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, @@ -639,7 +660,9 @@ public final class EmojiStatusSelectionController: ViewController { deviceMetrics: layout.deviceMetrics, emojiContent: emojiContent, backgroundColor: listBackgroundColor, - separatorColor: separatorColor + separatorColor: separatorColor, + hideTopPanel: false, + hideTopPanelUpdated: { _, _ in } )), environment: {}, containerSize: CGSize(width: componentWidth, height: min(308.0, layout.size.height)) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 5ec38ab6d5..3f93fbfd84 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -148,9 +148,11 @@ private final class WarpView: UIView { self.warpViews[i].update(containerSize: size, rect: rect, transition: transition) } - let frame = CGRect(origin: CGPoint(x: 0.0, y: -topInset), size: CGSize(width: size.width, height: topInset + size.height - 21.0)) + let clippingTopInset: CGFloat = topInset + let frame = CGRect(origin: CGPoint(x: 0.0, y: clippingTopInset), size: CGSize(width: size.width, height: -clippingTopInset + size.height - 21.0)) transition.setPosition(view: self.clippingView, position: frame.center) - transition.setBounds(view: self.clippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: -topInset), size: frame.size)) + transition.setBounds(view: self.clippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: clippingTopInset), size: frame.size)) + self.clippingView.clipsToBounds = true transition.setFrame(view: self.warpMaskContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - allItemsHeight), size: CGSize(width: size.width, height: allItemsHeight))) @@ -1500,74 +1502,214 @@ private final class GroupExpandActionButton: UIButton { } } -private final class SearchHeaderView: UIView, UITextFieldDelegate { - override static var layerClass: AnyClass { +public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate { + private final class EmojiSearchTextField: UITextField { + override func textRect(forBounds bounds: CGRect) -> CGRect { + return bounds.integral + } + } + + private struct Params: Equatable { + var theme: PresentationTheme + var strings: PresentationStrings + var useOpaqueTheme: Bool + var isActive: Bool + var size: CGSize + + static func ==(lhs: Params, rhs: Params) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.useOpaqueTheme != rhs.useOpaqueTheme { + return false + } + if lhs.isActive != rhs.isActive { + return false + } + if lhs.size != rhs.size { + return false + } + return true + } + } + + override public static var layerClass: AnyClass { return PassthroughLayer.self } - private let requestUpdate: () -> Void + private let activated: () -> Void + private let deactivated: () -> Void + private let updateQuery: (String) -> Void - let tintContainerLayer: SimpleLayer + let tintContainerView: UIView private let backgroundLayer: SimpleLayer private let tintBackgroundLayer: SimpleLayer - private var tapRecognizer: UITapGestureRecognizer? - private var textField: UITextField? + private let searchIconView: UIImageView + private let searchIconTintView: UIImageView + private let tintTextView: ComponentView + private let textView: ComponentView + private let cancelButtonTintTitle: ComponentView + private let cancelButtonTitle: ComponentView + private let cancelButton: HighlightTrackingButton - var wantsDisplayBelowKeyboard: Bool { + private var textField: EmojiSearchTextField? + + private var tapRecognizer: UITapGestureRecognizer? + + private var params: Params? + + public var wantsDisplayBelowKeyboard: Bool { return self.textField != nil } - init(requestUpdate: @escaping () -> Void) { - self.requestUpdate = requestUpdate + init(activated: @escaping () -> Void, deactivated: @escaping () -> Void, updateQuery: @escaping (String) -> Void) { + self.activated = activated + self.deactivated = deactivated + self.updateQuery = updateQuery - self.tintContainerLayer = SimpleLayer() + self.tintContainerView = UIView() self.backgroundLayer = SimpleLayer() self.tintBackgroundLayer = SimpleLayer() + self.searchIconView = UIImageView() + self.searchIconTintView = UIImageView() + + self.tintTextView = ComponentView() + self.textView = ComponentView() + self.cancelButtonTintTitle = ComponentView() + self.cancelButtonTitle = ComponentView() + self.cancelButton = HighlightTrackingButton() + super.init(frame: CGRect()) self.layer.addSublayer(self.backgroundLayer) - self.tintContainerLayer.addSublayer(self.tintBackgroundLayer) + self.tintContainerView.layer.addSublayer(self.tintBackgroundLayer) - (self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerLayer + self.addSubview(self.searchIconView) + self.tintContainerView.addSubview(self.searchIconTintView) + + self.addSubview(self.cancelButton) + self.clipsToBounds = true + + (self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerView.layer let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.tapRecognizer = tapRecognizer self.addGestureRecognizer(tapRecognizer) + + self.cancelButton.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view { + cancelButtonTitleView.layer.removeAnimation(forKey: "opacity") + cancelButtonTitleView.alpha = 0.4 + } + if let cancelButtonTintTitleView = strongSelf.cancelButtonTintTitle.view { + cancelButtonTintTitleView.layer.removeAnimation(forKey: "opacity") + cancelButtonTintTitleView.alpha = 0.4 + } + } else { + if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view { + cancelButtonTitleView.alpha = 1.0 + cancelButtonTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + if let cancelButtonTintTitleView = strongSelf.cancelButtonTintTitle.view { + cancelButtonTintTitleView.alpha = 1.0 + cancelButtonTintTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), for: .touchUpInside) } - required init?(coder: NSCoder) { + required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - /*if self.textField == nil { - let textField = UITextField(frame: self.backgroundLayer.frame.insetBy(dx: 10.0, dy: 0.0)) + if self.textField == nil, let textComponentView = self.textView.view { + let backgroundFrame = self.backgroundLayer.frame + let textFieldFrame = CGRect(origin: CGPoint(x: textComponentView.frame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textComponentView.frame.minX, height: backgroundFrame.height)) + + let textField = EmojiSearchTextField(frame: textFieldFrame) self.textField = textField self.addSubview(textField) textField.delegate = self - - self.requestUpdate() - textField.becomeFirstResponder() - }*/ + textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) + } + + self.activated() + + self.textField?.becomeFirstResponder() } } - func textFieldDidBeginEditing(_ textField: UITextField) { + @objc private func cancelPressed() { + self.updateQuery("") + + if let textField = self.textField { + self.textField = nil + + textField.resignFirstResponder() + textField.removeFromSuperview() + } + self.deactivated() } - func textFieldDidEndEditing(_ textField: UITextField) { + public func textFieldDidBeginEditing(_ textField: UITextField) { } - func update(theme: PresentationTheme, useOpaqueTheme: Bool, title: String, size: CGSize, transition: Transition) { + public func textFieldDidEndEditing(_ textField: UITextField) { + } + + @objc private func textFieldChanged(_ textField: UITextField) { + self.update(transition: .immediate) + + self.updateQuery(textField.text ?? "") + } + + private func update(transition: Transition) { + guard let params = self.params else { + return + } + self.params = nil + self.update(theme: params.theme, strings: params.strings, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, transition: transition) + } + + public func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, transition: Transition) { + let params = Params( + theme: theme, + strings: strings, + useOpaqueTheme: useOpaqueTheme, + isActive: isActive, + size: size + ) + + if self.params == params { + return + } + + if self.params?.theme !== theme { + self.searchIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor) + self.searchIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white) + } + + self.params = params + let sideInset: CGFloat = 8.0 let topInset: CGFloat = 8.0 let inputHeight: CGFloat = 36.0 + let sideTextInset: CGFloat = 8.0 + 4.0 + 24.0 + if useOpaqueTheme { self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor self.tintBackgroundLayer.backgroundColor = UIColor.white.cgColor @@ -1579,14 +1721,112 @@ private final class SearchHeaderView: UIView, UITextFieldDelegate { self.backgroundLayer.cornerRadius = inputHeight * 0.5 self.tintBackgroundLayer.cornerRadius = inputHeight * 0.5 - let backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight)) + //TODO:localize + let textSize = self.textView.update( + transition: .immediate, + component: AnyComponent(Text( + text: "Search Reactions", + font: Font.regular(17.0), + color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor + )), + environment: {}, + containerSize: CGSize(width: size.width - 32.0, height: 100.0) + ) + let _ = self.tintTextView.update( + transition: .immediate, + component: AnyComponent(Text( + text: "Search Reactions", + font: Font.regular(17.0), + color: .white + )), + environment: {}, + containerSize: CGSize(width: size.width - 32.0, height: 100.0) + ) + let cancelTextSize = self.cancelButtonTitle.update( + transition: .immediate, + component: AnyComponent(Text( + text: strings.Common_Cancel, + font: Font.regular(17.0), + color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor + )), + environment: {}, + containerSize: CGSize(width: size.width - 32.0, height: 100.0) + ) + let _ = self.cancelButtonTintTitle.update( + transition: .immediate, + component: AnyComponent(Text( + text: strings.Common_Cancel, + font: Font.regular(17.0), + color: .white + )), + environment: {}, + containerSize: CGSize(width: size.width - 32.0, height: 100.0) + ) + + let cancelButtonSpacing: CGFloat = 8.0 + + var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight)) + if isActive { + backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing + } transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) transition.setFrame(layer: self.tintBackgroundLayer, frame: backgroundFrame) - if let textField = self.textField { - textField.textColor = theme.chat.inputMediaPanel.panelContentControlOpaqueOverlayColor - textField.frame = backgroundFrame + transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height))) + + var textFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floor((backgroundFrame.width - textSize.width) / 2.0), y: backgroundFrame.minY + floor((backgroundFrame.height - textSize.height) / 2.0)), size: textSize) + if isActive { + textFrame.origin.x = backgroundFrame.minX + sideTextInset } + + if let image = self.searchIconView.image { + let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) + transition.setFrame(view: self.searchIconView, frame: iconFrame) + transition.setFrame(view: self.searchIconTintView, frame: iconFrame) + } + + if let textComponentView = self.textView.view { + if textComponentView.superview == nil { + self.addSubview(textComponentView) + textComponentView.isUserInteractionEnabled = false + } + transition.setFrame(view: textComponentView, frame: textFrame) + } + if let tintTextComponentView = self.tintTextView.view { + if tintTextComponentView.superview == nil { + self.tintContainerView.addSubview(tintTextComponentView) + tintTextComponentView.isUserInteractionEnabled = false + } + transition.setFrame(view: tintTextComponentView, frame: textFrame) + } + + if let cancelButtonTitleComponentView = self.cancelButtonTitle.view { + if cancelButtonTitleComponentView.superview == nil { + self.addSubview(cancelButtonTitleComponentView) + cancelButtonTitleComponentView.isUserInteractionEnabled = false + } + transition.setFrame(view: cancelButtonTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize)) + } + if let cancelButtonTintTitleComponentView = self.cancelButtonTintTitle.view { + if cancelButtonTintTitleComponentView.superview == nil { + self.tintContainerView.addSubview(cancelButtonTintTitleComponentView) + cancelButtonTintTitleComponentView.isUserInteractionEnabled = false + } + transition.setFrame(view: cancelButtonTintTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize)) + } + + var hasText = false + if let textField = self.textField { + textField.textColor = theme.contextMenu.primaryColor + transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideTextInset, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.width - sideTextInset, height: backgroundFrame.height))) + + if let text = textField.text, !text.isEmpty { + hasText = true + } + } + + self.tintTextView.view?.isHidden = hasText + self.textView.view?.isHidden = hasText } } @@ -1668,12 +1908,12 @@ public final class EmojiPagerContentComponent: Component { public let presentGlobalOverlayController: (ViewController) -> Void public let navigationController: () -> NavigationController? public let requestUpdate: (Transition) -> Void - public let sendSticker: ((FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Void)? + public let updateSearchQuery: (String) -> Void public let chatPeerId: PeerId? public let peekBehavior: EmojiContentPeekBehavior? public let customLayout: CustomLayout? public let externalBackground: ExternalBackground? - public let externalExpansionView: UIView? + public weak var externalExpansionView: UIView? public let useOpaqueTheme: Bool public init( @@ -1688,7 +1928,7 @@ public final class EmojiPagerContentComponent: Component { presentGlobalOverlayController: @escaping (ViewController) -> Void, navigationController: @escaping () -> NavigationController?, requestUpdate: @escaping (Transition) -> Void, - sendSticker: ((FileMediaReference, Bool, Bool, String?, Bool, UIView, CGRect, CALayer?, [ItemCollectionId]) -> Void)?, + updateSearchQuery: @escaping (String) -> Void, chatPeerId: PeerId?, peekBehavior: EmojiContentPeekBehavior?, customLayout: CustomLayout?, @@ -1707,7 +1947,7 @@ public final class EmojiPagerContentComponent: Component { self.presentGlobalOverlayController = presentGlobalOverlayController self.navigationController = navigationController self.requestUpdate = requestUpdate - self.sendSticker = sendSticker + self.updateSearchQuery = updateSearchQuery self.chatPeerId = chatPeerId self.peekBehavior = peekBehavior self.customLayout = customLayout @@ -1917,6 +2157,7 @@ public final class EmojiPagerContentComponent: Component { public let inputInteractionHolder: InputInteractionHolder public let itemGroups: [ItemGroup] public let itemLayoutType: ItemLayoutType + public let itemContentUniqueId: AnyHashable? public let warpContentsOnEdges: Bool public let displaySearch: Bool public let enableLongPress: Bool @@ -1931,6 +2172,7 @@ public final class EmojiPagerContentComponent: Component { inputInteractionHolder: InputInteractionHolder, itemGroups: [ItemGroup], itemLayoutType: ItemLayoutType, + itemContentUniqueId: AnyHashable?, warpContentsOnEdges: Bool, displaySearch: Bool, enableLongPress: Bool, @@ -1944,13 +2186,14 @@ public final class EmojiPagerContentComponent: Component { self.inputInteractionHolder = inputInteractionHolder self.itemGroups = itemGroups self.itemLayoutType = itemLayoutType + self.itemContentUniqueId = itemContentUniqueId self.warpContentsOnEdges = warpContentsOnEdges self.displaySearch = displaySearch self.enableLongPress = enableLongPress self.selectedItems = selectedItems } - public func withUpdatedItemGroups(_ itemGroups: [ItemGroup]) -> EmojiPagerContentComponent { + public func withUpdatedItemGroups(itemGroups: [ItemGroup], itemContentUniqueId: AnyHashable?) -> EmojiPagerContentComponent { return EmojiPagerContentComponent( id: self.id, context: self.context, @@ -1960,6 +2203,7 @@ public final class EmojiPagerContentComponent: Component { inputInteractionHolder: self.inputInteractionHolder, itemGroups: itemGroups, itemLayoutType: self.itemLayoutType, + itemContentUniqueId: itemContentUniqueId, warpContentsOnEdges: self.warpContentsOnEdges, displaySearch: self.displaySearch, enableLongPress: self.enableLongPress, @@ -2067,7 +2311,7 @@ public final class EmojiPagerContentComponent: Component { var premiumButtonInset: CGFloat var premiumButtonHeight: CGFloat - init(layoutType: ItemLayoutType, width: CGFloat, containerInsets: UIEdgeInsets, itemGroups: [ItemGroupDescription], expandedGroupIds: Set, curveNearBounds: Bool, displaySearch: Bool, customLayout: CustomLayout?) { + init(layoutType: ItemLayoutType, width: CGFloat, containerInsets: UIEdgeInsets, itemGroups: [ItemGroupDescription], expandedGroupIds: Set, curveNearBounds: Bool, displaySearch: Bool, isSearchActivated: Bool, customLayout: CustomLayout?) { self.layoutType = layoutType self.width = width @@ -2758,6 +3002,8 @@ public final class EmojiPagerContentComponent: Component { private let shimmerHostView: PortalSourceView? private let standaloneShimmerEffect: StandaloneShimmerEffect? + private var isSearchActivated: Bool = false + private let backgroundView: BlurredBackgroundView private var vibrancyEffectView: UIVisualEffectView? public private(set) var mirrorContentClippingView: UIView? @@ -2770,7 +3016,7 @@ public final class EmojiPagerContentComponent: Component { private var effectiveVisibleSize: CGSize = CGSize() private let placeholdersContainerView: UIView - private var visibleSearchHeader: SearchHeaderView? + private var visibleSearchHeader: EmojiSearchHeaderView? private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:] private var visibleItemSelectionLayers: [ItemLayer.Key: ItemSelectionLayer] = [:] private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:] @@ -2791,6 +3037,8 @@ public final class EmojiPagerContentComponent: Component { private var contextFocusItemKey: EmojiPagerContentComponent.View.ItemLayer.Key? + private var contextGesture: ContextGesture? + private var tapRecognizer: UITapGestureRecognizer? private var longTapRecognizer: UILongPressGestureRecognizer? override init(frame: CGRect) { @@ -2806,7 +3054,7 @@ public final class EmojiPagerContentComponent: Component { self.mirrorContentScrollView = UIView() self.mirrorContentScrollView.layer.anchorPoint = CGPoint() - self.mirrorContentScrollView.clipsToBounds = false + self.mirrorContentScrollView.clipsToBounds = true self.scrollView = ContentScrollView(mirrorView: self.mirrorContentScrollView) self.scrollView.layer.anchorPoint = CGPoint() @@ -2962,9 +3210,12 @@ public final class EmojiPagerContentComponent: Component { let _ = foundItem } + self.contextGesture = contextGesture self.addGestureRecognizer(contextGesture) - //self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) + self.tapRecognizer = tapRecognizer + self.addGestureRecognizer(tapRecognizer) let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:))) longTapRecognizer.minimumPressDuration = 0.2 @@ -3125,6 +3376,437 @@ public final class EmojiPagerContentComponent: Component { } } + private func scrollToTop() { + guard let _ = self.component, let _ = self.pagerEnvironment, let itemLayout = self.itemLayout else { + return + } + if itemLayout.itemGroupLayouts.isEmpty { + return + } + + if "".isEmpty { + let wasIgnoringScrollingEvents = self.ignoreScrolling + self.ignoreScrolling = true + self.scrollView.setContentOffset(self.scrollView.contentOffset, animated: false) + + self.keepTopPanelVisibleUntilScrollingInput = true + + let scrollPosition: CGFloat = 0.0 + + let offsetDirectionSign: Double = scrollPosition < self.scrollView.bounds.minY ? -1.0 : 1.0 + + var previousVisibleLayers: [ItemLayer.Key: (CALayer, CGRect)] = [:] + for (id, layer) in self.visibleItemLayers { + previousVisibleLayers[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) + } + var previousVisibleItemSelectionLayers: [ItemLayer.Key: (CALayer, CGRect)] = [:] + for (id, layer) in self.visibleItemSelectionLayers { + previousVisibleItemSelectionLayers[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) + } + var previousVisiblePlaceholderViews: [ItemLayer.Key: (UIView, CGRect)] = [:] + for (id, view) in self.visibleItemPlaceholderViews { + previousVisiblePlaceholderViews[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) + } + var previousVisibleGroupHeaders: [AnyHashable: (GroupHeaderLayer, CGRect)] = [:] + for (id, view) in self.visibleGroupHeaders { + if !self.scrollView.bounds.intersects(view.frame) { + continue + } + previousVisibleGroupHeaders[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) + } + var previousVisibleGroupBorders: [AnyHashable: (GroupBorderLayer, CGRect)] = [:] + for (id, layer) in self.visibleGroupBorders { + previousVisibleGroupBorders[id] = (layer, layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) + } + var previousVisibleGroupPremiumButtons: [AnyHashable: (UIView, CGRect)] = [:] + for (id, view) in self.visibleGroupPremiumButtons { + if let view = view.view { + previousVisibleGroupPremiumButtons[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) + } + } + var previousVisibleGroupExpandActionButtons: [AnyHashable: (GroupExpandActionButton, CGRect)] = [:] + for (id, view) in self.visibleGroupExpandActionButtons { + previousVisibleGroupExpandActionButtons[id] = (view, view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY)) + } + + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: scrollPosition), size: self.scrollView.bounds.size) + self.ignoreScrolling = wasIgnoringScrollingEvents + + self.updateVisibleItems(transition: .immediate, attemptSynchronousLoads: true, previousItemPositions: nil, updatedItemPositions: nil) + + var commonItemOffset: CGFloat? + var previousVisibleBoundingRect: CGRect? + for (id, layerAndFrame) in previousVisibleLayers { + if let layer = self.visibleItemLayers[id] { + if commonItemOffset == nil { + let visibleFrame = layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + commonItemOffset = layerAndFrame.1.minY - visibleFrame.minY + } + break + } else { + if let previousVisibleBoundingRectValue = previousVisibleBoundingRect { + previousVisibleBoundingRect = layerAndFrame.1.union(previousVisibleBoundingRectValue) + } else { + previousVisibleBoundingRect = layerAndFrame.1 + } + } + } + + for (id, viewAndFrame) in previousVisiblePlaceholderViews { + if let view = self.visibleItemPlaceholderViews[id] { + if commonItemOffset == nil { + let visibleFrame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + commonItemOffset = viewAndFrame.1.minY - visibleFrame.minY + } + break + } else { + if let previousVisibleBoundingRectValue = previousVisibleBoundingRect { + previousVisibleBoundingRect = viewAndFrame.1.union(previousVisibleBoundingRectValue) + } else { + previousVisibleBoundingRect = viewAndFrame.1 + } + } + } + + for (id, layerAndFrame) in previousVisibleGroupHeaders { + if let view = self.visibleGroupHeaders[id] { + if commonItemOffset == nil, self.scrollView.bounds.intersects(view.frame) { + let visibleFrame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + commonItemOffset = layerAndFrame.1.minY - visibleFrame.minY + } + break + } else { + if let previousVisibleBoundingRectValue = previousVisibleBoundingRect { + previousVisibleBoundingRect = layerAndFrame.1.union(previousVisibleBoundingRectValue) + } else { + previousVisibleBoundingRect = layerAndFrame.1 + } + } + } + + for (id, viewAndFrame) in previousVisibleGroupPremiumButtons { + if let view = self.visibleGroupPremiumButtons[id]?.view, self.scrollView.bounds.intersects(view.frame) { + if commonItemOffset == nil { + let visibleFrame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + commonItemOffset = viewAndFrame.1.minY - visibleFrame.minY + } + break + } else { + if let previousVisibleBoundingRectValue = previousVisibleBoundingRect { + previousVisibleBoundingRect = viewAndFrame.1.union(previousVisibleBoundingRectValue) + } else { + previousVisibleBoundingRect = viewAndFrame.1 + } + } + } + + for (id, viewAndFrame) in previousVisibleGroupExpandActionButtons { + if let view = self.visibleGroupExpandActionButtons[id], self.scrollView.bounds.intersects(view.frame) { + if commonItemOffset == nil { + let visibleFrame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + commonItemOffset = viewAndFrame.1.minY - visibleFrame.minY + } + break + } else { + if let previousVisibleBoundingRectValue = previousVisibleBoundingRect { + previousVisibleBoundingRect = viewAndFrame.1.union(previousVisibleBoundingRectValue) + } else { + previousVisibleBoundingRect = viewAndFrame.1 + } + } + } + + let duration = 0.4 + let timingFunction = kCAMediaTimingFunctionSpring + + if let commonItemOffset = commonItemOffset { + for (_, layer) in self.visibleItemLayers { + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, layerAndFrame) in previousVisibleLayers { + if self.visibleItemLayers[id] != nil { + continue + } + let layer = layerAndFrame.0 + self.scrollView.layer.addSublayer(layer) + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in + layer?.removeFromSuperlayer() + }) + } + + for (_, view) in self.visibleItemPlaceholderViews { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, viewAndFrame) in previousVisiblePlaceholderViews { + if self.visibleItemPlaceholderViews[id] != nil { + continue + } + let view = viewAndFrame.0 + self.placeholdersContainerView.addSubview(view) + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + + for (_, view) in self.visibleGroupHeaders { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, viewAndFrame) in previousVisibleGroupHeaders { + if self.visibleGroupHeaders[id] != nil { + continue + } + let view = viewAndFrame.0 + self.scrollView.addSubview(view) + let tintContentLayer = view.tintContentLayer + self.mirrorContentScrollView.layer.addSublayer(tintContentLayer) + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view, weak tintContentLayer] _ in + view?.removeFromSuperview() + tintContentLayer?.removeFromSuperlayer() + }) + } + + for (_, layer) in self.visibleGroupBorders { + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, layerAndFrame) in previousVisibleGroupBorders { + if self.visibleGroupBorders[id] != nil { + continue + } + let layer = layerAndFrame.0 + self.scrollView.layer.addSublayer(layer) + let tintContainerLayer = layer.tintContainerLayer + self.mirrorContentScrollView.layer.addSublayer(tintContainerLayer) + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer, weak tintContainerLayer] _ in + layer?.removeFromSuperlayer() + tintContainerLayer?.removeFromSuperlayer() + }) + } + + for (_, view) in self.visibleGroupPremiumButtons { + if let view = view.view { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + } + for (id, viewAndFrame) in previousVisibleGroupPremiumButtons { + if self.visibleGroupPremiumButtons[id] != nil { + continue + } + let view = viewAndFrame.0 + self.scrollView.addSubview(view) + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + + for (_, view) in self.visibleGroupExpandActionButtons { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, viewAndFrame) in previousVisibleGroupExpandActionButtons { + if self.visibleGroupExpandActionButtons[id] != nil { + continue + } + let view = viewAndFrame.0 + self.scrollView.addSubview(view) + let tintContainerLayer = view.tintContainerLayer + self.mirrorContentScrollView.layer.addSublayer(tintContainerLayer) + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view, weak tintContainerLayer] _ in + view?.removeFromSuperview() + tintContainerLayer?.removeFromSuperlayer() + }) + } + } else if let previousVisibleBoundingRect = previousVisibleBoundingRect { + var updatedVisibleBoundingRect: CGRect? + + for (_, layer) in self.visibleItemLayers { + let frame = layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect { + updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue) + } else { + updatedVisibleBoundingRect = frame + } + } + for (_, view) in self.visibleItemPlaceholderViews { + let frame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect { + updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue) + } else { + updatedVisibleBoundingRect = frame + } + } + for (_, view) in self.visibleGroupHeaders { + if !self.scrollView.bounds.intersects(view.frame) { + continue + } + let frame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect { + updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue) + } else { + updatedVisibleBoundingRect = frame + } + } + for (_, view) in self.visibleGroupPremiumButtons { + if let view = view.view { + if !self.scrollView.bounds.intersects(view.frame) { + continue + } + + let frame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect { + updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue) + } else { + updatedVisibleBoundingRect = frame + } + } + } + for (_, view) in self.visibleGroupExpandActionButtons { + if !self.scrollView.bounds.intersects(view.frame) { + continue + } + + let frame = view.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) + if let updatedVisibleBoundingRectValue = updatedVisibleBoundingRect { + updatedVisibleBoundingRect = frame.union(updatedVisibleBoundingRectValue) + } else { + updatedVisibleBoundingRect = frame + } + } + + if let updatedVisibleBoundingRect = updatedVisibleBoundingRect { + var commonItemOffset = updatedVisibleBoundingRect.height * offsetDirectionSign + + if previousVisibleBoundingRect.intersects(updatedVisibleBoundingRect) { + if offsetDirectionSign < 0.0 { + commonItemOffset = previousVisibleBoundingRect.minY - updatedVisibleBoundingRect.maxY + } else { + commonItemOffset = previousVisibleBoundingRect.maxY - updatedVisibleBoundingRect.minY + } + } + + for (_, layer) in self.visibleItemLayers { + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (_, layer) in self.visibleItemSelectionLayers { + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, layerAndFrame) in previousVisibleLayers { + if self.visibleItemLayers[id] != nil { + continue + } + let layer = layerAndFrame.0 + layer.frame = layerAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + self.scrollView.layer.addSublayer(layer) + layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in + layer?.removeFromSuperlayer() + }) + } + for (id, layerAndFrame) in previousVisibleItemSelectionLayers { + if self.visibleItemSelectionLayers[id] != nil { + continue + } + let layer = layerAndFrame.0 + layer.frame = layerAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + self.scrollView.layer.addSublayer(layer) + layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in + layer?.removeFromSuperlayer() + }) + } + + for (_, view) in self.visibleItemPlaceholderViews { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, viewAndFrame) in previousVisiblePlaceholderViews { + if self.visibleItemPlaceholderViews[id] != nil { + continue + } + let view = viewAndFrame.0 + view.frame = viewAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + self.placeholdersContainerView.addSubview(view) + view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + + for (_, view) in self.visibleGroupHeaders { + if !self.scrollView.bounds.intersects(view.frame) { + continue + } + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, viewAndFrame) in previousVisibleGroupHeaders { + if self.visibleGroupHeaders[id] != nil { + continue + } + let view = viewAndFrame.0 + view.frame = viewAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + self.scrollView.addSubview(view) + let tintContentLayer = view.tintContentLayer + self.mirrorContentScrollView.layer.addSublayer(tintContentLayer) + view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view, weak tintContentLayer] _ in + view?.removeFromSuperview() + tintContentLayer?.removeFromSuperlayer() + }) + } + + for (_, layer) in self.visibleGroupBorders { + if !self.scrollView.bounds.intersects(layer.frame) { + continue + } + layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, layerAndFrame) in previousVisibleGroupBorders { + if self.visibleGroupBorders[id] != nil { + continue + } + let layer = layerAndFrame.0 + layer.frame = layerAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + self.scrollView.layer.addSublayer(layer) + let tintContainerLayer = layer.tintContainerLayer + self.mirrorContentScrollView.layer.addSublayer(tintContainerLayer) + layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak layer, weak tintContainerLayer] _ in + layer?.removeFromSuperlayer() + tintContainerLayer?.removeFromSuperlayer() + }) + } + + for (_, view) in self.visibleGroupPremiumButtons { + if let view = view.view { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + } + for (id, viewAndFrame) in previousVisibleGroupPremiumButtons { + if self.visibleGroupPremiumButtons[id] != nil { + continue + } + let view = viewAndFrame.0 + view.frame = viewAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + self.scrollView.addSubview(view) + view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + + for (_, view) in self.visibleGroupExpandActionButtons { + view.layer.animatePosition(from: CGPoint(x: 0.0, y: commonItemOffset), to: CGPoint(), duration: duration, timingFunction: timingFunction, additive: true) + } + for (id, viewAndFrame) in previousVisibleGroupExpandActionButtons { + if self.visibleGroupExpandActionButtons[id] != nil { + continue + } + let view = viewAndFrame.0 + view.frame = viewAndFrame.1.offsetBy(dx: 0.0, dy: self.scrollView.bounds.minY) + self.scrollView.addSubview(view) + let tintContainerLayer = view.tintContainerLayer + self.mirrorContentScrollView.layer.addSublayer(tintContainerLayer) + view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -commonItemOffset), duration: duration, timingFunction: timingFunction, removeOnCompletion: false, additive: true, completion: { [weak view, weak tintContainerLayer] _ in + view?.removeFromSuperview() + tintContainerLayer?.removeFromSuperlayer() + }) + } + } + } + } + } + public func scrollToItemGroup(id supergroupId: AnyHashable, subgroupId: Int32?) { guard let component = self.component, let pagerEnvironment = self.pagerEnvironment, let itemLayout = self.itemLayout else { return @@ -3259,22 +3941,6 @@ public final class EmojiPagerContentComponent: Component { } } - /*for (id, layerAndFrame) in previousVisibleGroupBorders { - if let layer = self.visibleGroupBorders[id] { - if commonItemOffset == nil, self.scrollView.bounds.intersects(layer.frame) { - let visibleFrame = layer.frame.offsetBy(dx: 0.0, dy: -self.scrollView.bounds.minY) - commonItemOffset = layerAndFrame.1.minY - visibleFrame.minY - } - break - } else { - if let previousVisibleBoundingRectValue = previousVisibleBoundingRect { - previousVisibleBoundingRect = layerAndFrame.1.union(previousVisibleBoundingRectValue) - } else { - previousVisibleBoundingRect = layerAndFrame.1 - } - } - }*/ - for (id, viewAndFrame) in previousVisibleGroupPremiumButtons { if let view = self.visibleGroupPremiumButtons[id]?.view, self.scrollView.bounds.intersects(view.frame) { if commonItemOffset == nil { @@ -3600,11 +4266,11 @@ public final class EmojiPagerContentComponent: Component { } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { - guard let _ = self.component else { + guard let component = self.component else { return } if case .ended = recognizer.state { - /*let locationInScrollView = recognizer.location(in: self.scrollView) + let locationInScrollView = recognizer.location(in: self.scrollView) outer: for (id, groupHeader) in self.visibleGroupHeaders { if groupHeader.frame.insetBy(dx: -10.0, dy: -6.0).contains(locationInScrollView) { let groupHeaderPoint = self.scrollView.convert(locationInScrollView, to: groupHeader) @@ -3612,7 +4278,7 @@ public final class EmojiPagerContentComponent: Component { component.inputInteractionHolder.inputInteraction?.clearGroup(id) return } else { - if groupHeader.tapGesture(recognizer) { + if groupHeader.tapGesture(point: recognizer.location(in: groupHeader)) { return } } @@ -3638,7 +4304,7 @@ public final class EmojiPagerContentComponent: Component { } } - let _ = foundItem*/ + let _ = foundItem } } @@ -3859,7 +4525,7 @@ public final class EmojiPagerContentComponent: Component { } private func updateScrollingOffset(isReset: Bool, transition: Transition) { - guard let _ = self.component else { + guard let component = self.component else { return } @@ -3870,7 +4536,7 @@ public final class EmojiPagerContentComponent: Component { let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY) let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value - //if case .detailed = component.itemLayoutType { + if !component.warpContentsOnEdges { self.pagerEnvironment?.onChildScrollingUpdate(PagerComponentChildEnvironment.ContentScrollingUpdate( relativeOffset: relativeOffset, absoluteOffsetToTopEdge: offsetToTopEdge, @@ -3879,7 +4545,7 @@ public final class EmojiPagerContentComponent: Component { isInteracting: isInteracting, transition: transition )) - //} + } } self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting) } @@ -3946,33 +4612,6 @@ public final class EmojiPagerContentComponent: Component { } } - if component.displaySearch { - let visibleSearchHeader: SearchHeaderView - if let current = self.visibleSearchHeader { - visibleSearchHeader = current - } else { - visibleSearchHeader = SearchHeaderView(requestUpdate: { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate) - }) - self.visibleSearchHeader = visibleSearchHeader - self.scrollView.addSubview(visibleSearchHeader) - self.mirrorContentScrollView.layer.addSublayer(visibleSearchHeader.tintContainerLayer) - } - - let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight)) - visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, useOpaqueTheme: useOpaqueTheme, title: keyboardChildEnvironment.strings.Common_Search, size: searchHeaderFrame.size, transition: transition) - transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame) - } else { - if let visibleSearchHeader = self.visibleSearchHeader { - self.visibleSearchHeader = nil - visibleSearchHeader.removeFromSuperview() - visibleSearchHeader.tintContainerLayer.removeFromSuperlayer() - } - } - for groupItems in itemLayout.visibleItems(for: effectiveVisibleBounds) { let itemGroup = component.itemGroups[groupItems.groupIndex] let itemGroupLayout = itemLayout.itemGroupLayouts[groupItems.groupIndex] @@ -4723,7 +5362,7 @@ public final class EmojiPagerContentComponent: Component { } public func pagerUpdateBackground(backgroundFrame: CGRect, transition: Transition) { - guard let component = self.component, let keyboardChildEnvironment = self.keyboardChildEnvironment else { + guard let component = self.component, let keyboardChildEnvironment = self.keyboardChildEnvironment, let pagerEnvironment = self.pagerEnvironment else { return } @@ -4743,9 +5382,9 @@ public final class EmojiPagerContentComponent: Component { } } - let clippingFrame = CGRect(origin: CGPoint(x: 0.0, y: 42.0), size: CGSize(width: backgroundFrame.width, height: backgroundFrame.height)) + let clippingFrame = CGRect(origin: CGPoint(x: 0.0, y: pagerEnvironment.containerInsets.top), size: CGSize(width: backgroundFrame.width, height: backgroundFrame.height)) transition.setPosition(view: mirrorContentClippingView, position: clippingFrame.center) - transition.setBounds(view: mirrorContentClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: 42.0), size: clippingFrame.size)) + transition.setBounds(view: mirrorContentClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: pagerEnvironment.containerInsets.top), size: clippingFrame.size)) if mirrorContentClippingView.superview !== effectContainerView { effectContainerView.addSubview(mirrorContentClippingView) @@ -4815,6 +5454,12 @@ public final class EmojiPagerContentComponent: Component { if let longTapRecognizer = self.longTapRecognizer { longTapRecognizer.isEnabled = component.enableLongPress } + if let tapRecognizer = self.tapRecognizer { + tapRecognizer.isEnabled = component.enableLongPress + } + if let contextGesture = self.contextGesture { + contextGesture.isEnabled = !component.enableLongPress + } if let shimmerHostView = self.shimmerHostView { transition.setFrame(view: shimmerHostView, frame: CGRect(origin: CGPoint(), size: availableSize)) @@ -4969,7 +5614,7 @@ public final class EmojiPagerContentComponent: Component { var itemTransition = transition - let itemLayout = ItemLayout( + let extractedExpr = ItemLayout( layoutType: component.itemLayoutType, width: availableSize.width, containerInsets: UIEdgeInsets(top: pagerEnvironment.containerInsets.top + 9.0, left: pagerEnvironment.containerInsets.left, bottom: 9.0 + pagerEnvironment.containerInsets.bottom, right: pagerEnvironment.containerInsets.right), @@ -4977,13 +5622,18 @@ public final class EmojiPagerContentComponent: Component { expandedGroupIds: self.expandedGroupIds, curveNearBounds: component.warpContentsOnEdges, displaySearch: component.displaySearch, + isSearchActivated: self.isSearchActivated, customLayout: component.inputInteractionHolder.inputInteraction?.customLayout ) + let itemLayout = extractedExpr if let previousItemLayout = self.itemLayout { if previousItemLayout.width != itemLayout.width { itemTransition = .immediate } else if transition.userData(ContentAnimation.self) == nil { - itemTransition = .immediate + if previousItemLayout.itemInsets.top != itemLayout.itemInsets.top { + } else { + itemTransition = .immediate + } } } else { itemTransition = .immediate @@ -4991,26 +5641,41 @@ public final class EmojiPagerContentComponent: Component { self.itemLayout = itemLayout self.ignoreScrolling = true - transition.setPosition(view: self.scrollView, position: CGPoint()) + + let scrollOriginY: CGFloat = 0.0 + let scrollSize = CGSize(width: availableSize.width, height: availableSize.height) + + transition.setPosition(view: self.scrollView, position: CGPoint(x: 0.0, y: scrollOriginY)) let previousSize = self.scrollView.bounds.size - self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: availableSize) + var resetScrolling = false + if self.scrollView.bounds.isEmpty && component.displaySearch { + resetScrolling = true + } + if previousComponent?.itemContentUniqueId != component.itemContentUniqueId { + resetScrolling = true + } + self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollSize) let warpHeight: CGFloat = 50.0 + var topWarpInset = pagerEnvironment.containerInsets.top + if self.isSearchActivated { + topWarpInset += itemLayout.searchInsets.top + itemLayout.searchHeight + } if let warpView = self.warpView { transition.setFrame(view: warpView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)) - warpView.update(size: CGSize(width: availableSize.width, height: availableSize.height), topInset: pagerEnvironment.containerInsets.top, warpHeight: warpHeight, theme: keyboardChildEnvironment.theme, transition: transition) + warpView.update(size: CGSize(width: availableSize.width, height: availableSize.height), topInset: topWarpInset, warpHeight: warpHeight, theme: keyboardChildEnvironment.theme, transition: transition) } if let mirrorContentWarpView = self.mirrorContentWarpView { transition.setFrame(view: mirrorContentWarpView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: availableSize)) - mirrorContentWarpView.update(size: CGSize(width: availableSize.width, height: availableSize.height), topInset: pagerEnvironment.containerInsets.top, warpHeight: warpHeight, theme: keyboardChildEnvironment.theme, transition: transition) + mirrorContentWarpView.update(size: CGSize(width: availableSize.width, height: availableSize.height), topInset: topWarpInset, warpHeight: warpHeight, theme: keyboardChildEnvironment.theme, transition: transition) } - if availableSize.height > previousSize.height || transition.animation.isImmediate { + if scrollSize.height > previousSize.height || transition.animation.isImmediate { self.boundsChangeTrackerLayer.removeAllAnimations() self.boundsChangeTrackerLayer.bounds = self.scrollView.bounds self.effectiveVisibleSize = self.scrollView.bounds.size } else { - self.effectiveVisibleSize = CGSize(width: availableSize.width, height: max(self.effectiveVisibleSize.height, availableSize.height)) + self.effectiveVisibleSize = CGSize(width: scrollSize.width, height: max(self.effectiveVisibleSize.height, scrollSize.height)) transition.setBounds(layer: self.boundsChangeTrackerLayer, bounds: self.scrollView.bounds, completion: { [weak self] completed in guard let strongSelf = self else { return @@ -5081,6 +5746,14 @@ public final class EmojiPagerContentComponent: Component { } } + if resetScrolling { + if component.displaySearch && !self.isSearchActivated { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 50.0), size: scrollSize) + } else { + self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize) + } + } + self.ignoreScrolling = false if calculateUpdatedItemPositions { @@ -5115,12 +5788,82 @@ public final class EmojiPagerContentComponent: Component { } var attemptSynchronousLoads = !(scrollView.isDragging || scrollView.isDecelerating) + if resetScrolling { + attemptSynchronousLoads = true + } if let synchronousLoadBehavior = transition.userData(SynchronousLoadBehavior.self) { if synchronousLoadBehavior.isDisabled { attemptSynchronousLoads = false } } + if component.displaySearch { + let visibleSearchHeader: EmojiSearchHeaderView + if let current = self.visibleSearchHeader { + visibleSearchHeader = current + + if self.isSearchActivated { + if visibleSearchHeader.superview != self { + self.addSubview(visibleSearchHeader) + self.mirrorContentClippingView?.addSubview(visibleSearchHeader.tintContainerView) + } + } + } else { + visibleSearchHeader = EmojiSearchHeaderView(activated: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.isSearchActivated = true + strongSelf.pagerEnvironment?.onWantsExclusiveModeUpdated(true) + strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate) + }, deactivated: { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.scrollToTop() + + strongSelf.isSearchActivated = false + strongSelf.pagerEnvironment?.onWantsExclusiveModeUpdated(false) + strongSelf.component?.inputInteractionHolder.inputInteraction?.requestUpdate(.immediate) + }, updateQuery: { [weak self] query in + guard let strongSelf = self else { + return + } + strongSelf.component?.inputInteractionHolder.inputInteraction?.updateSearchQuery(query) + }) + self.visibleSearchHeader = visibleSearchHeader + if self.isSearchActivated { + self.addSubview(visibleSearchHeader) + self.mirrorContentClippingView?.addSubview(visibleSearchHeader.tintContainerView) + } else { + self.scrollView.addSubview(visibleSearchHeader) + self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) + } + } + + let useOpaqueTheme = component.inputInteractionHolder.inputInteraction?.useOpaqueTheme ?? false + + let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight)) + visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, transition: transition) + transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] _ in + guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else { + return + } + + if !strongSelf.isSearchActivated && visibleSearchHeader.superview != strongSelf.scrollView { + strongSelf.scrollView.addSubview(visibleSearchHeader) + strongSelf.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) + } + }) + } else { + if let visibleSearchHeader = self.visibleSearchHeader { + self.visibleSearchHeader = nil + visibleSearchHeader.removeFromSuperview() + visibleSearchHeader.tintContainerView.removeFromSuperview() + } + } + self.updateVisibleItems(transition: itemTransition, attemptSynchronousLoads: attemptSynchronousLoads, previousItemPositions: previousItemPositions, previousAbsoluteItemPositions: previousAbsoluteItemPositions, updatedItemPositions: updatedItemPositions, hintDisappearingGroupFrame: hintDisappearingGroupFrame) return availableSize @@ -5832,8 +6575,9 @@ public final class EmojiPagerContentComponent: Component { ) }, itemLayoutType: .compact, + itemContentUniqueId: nil, warpContentsOnEdges: isReactionSelection || isStatusSelection, - displaySearch: false, + displaySearch: isReactionSelection, enableLongPress: (isReactionSelection && !isQuickReactionSelection) || isStatusSelection, selectedItems: selectedItems ) diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift index 5a72b21aa4..0fd71a1118 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EntityKeyboard.swift @@ -99,6 +99,7 @@ public final class EntityKeyboardComponent: Component { public let externalTopPanelContainer: PagerExternalTopPanelContainer? public let topPanelExtensionUpdated: (CGFloat, Transition) -> Void public let hideInputUpdated: (Bool, Bool, Transition) -> Void + public let hideTopPanelUpdated: (Bool, Transition) -> Void public let switchToTextInput: () -> Void public let switchToGifSubject: (GifPagerContentComponent.Subject) -> Void public let reorderItems: (ReorderCategory, [EntityKeyboardTopPanelComponent.Item]) -> Void @@ -123,6 +124,7 @@ public final class EntityKeyboardComponent: Component { externalTopPanelContainer: PagerExternalTopPanelContainer?, topPanelExtensionUpdated: @escaping (CGFloat, Transition) -> Void, hideInputUpdated: @escaping (Bool, Bool, Transition) -> Void, + hideTopPanelUpdated: @escaping (Bool, Transition) -> Void, switchToTextInput: @escaping () -> Void, switchToGifSubject: @escaping (GifPagerContentComponent.Subject) -> Void, reorderItems: @escaping (ReorderCategory, [EntityKeyboardTopPanelComponent.Item]) -> Void, @@ -146,6 +148,7 @@ public final class EntityKeyboardComponent: Component { self.externalTopPanelContainer = externalTopPanelContainer self.topPanelExtensionUpdated = topPanelExtensionUpdated self.hideInputUpdated = hideInputUpdated + self.hideTopPanelUpdated = hideTopPanelUpdated self.switchToTextInput = switchToTextInput self.switchToGifSubject = switchToGifSubject self.reorderItems = reorderItems @@ -222,6 +225,7 @@ public final class EntityKeyboardComponent: Component { private var topPanelExtension: CGFloat? private var isTopPanelExpanded: Bool = false + private var isTopPanelHidden: Bool = false public var centralId: AnyHashable? { if let pagerView = self.pagerView.findTaggedView(tag: PagerComponentViewTag()) as? PagerComponent.View { @@ -626,6 +630,12 @@ public final class EntityKeyboardComponent: Component { } strongSelf.isTopPanelExpandedUpdated(isExpanded: isExpanded, transition: transition) }, + isTopPanelHiddenUpdated: { [weak self] isTopPanelHidden, transition in + guard let strongSelf = self else { + return + } + strongSelf.isTopPanelHiddenUpdated(isTopPanelHidden: isTopPanelHidden, transition: transition) + }, panelHideBehavior: panelHideBehavior )), environment: { @@ -727,6 +737,18 @@ public final class EntityKeyboardComponent: Component { component.hideInputUpdated(self.isTopPanelExpanded, false, transition) } + private func isTopPanelHiddenUpdated(isTopPanelHidden: Bool, transition: Transition) { + if self.isTopPanelHidden != isTopPanelHidden { + self.isTopPanelHidden = isTopPanelHidden + } + + guard let component = self.component else { + return + } + + component.hideTopPanelUpdated(self.isTopPanelHidden, transition) + } + private func openSearch() { guard let component = self.component else { return diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index d431683d97..fb914d8c93 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -110,6 +110,10 @@ private class ApplicationStatusBarHost: StatusBarHost { } var keyboardWindow: UIWindow? { + if #available(iOS 16.0, *) { + return UIApplication.shared.internalGetKeyboard() + } + for window in UIApplication.shared.windows { if isKeyboardWindow(window: window) { return window diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index fe4c7fbc34..521193756a 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -1237,6 +1237,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } + controller?.view.endEditing(true) + let chosenReaction: MessageReaction.Reaction = chosenUpdatedReaction.reaction let currentReactions = mergedMessageReactions(attributes: message.attributes)?.reactions ?? [] diff --git a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift index a723ac7633..7c01f8f8eb 100644 --- a/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift @@ -535,6 +535,7 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { ) }, itemLayoutType: .detailed, + itemContentUniqueId: nil, warpContentsOnEdges: false, displaySearch: false, enableLongPress: false, @@ -1120,13 +1121,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return controllerInteraction?.navigationController() }, requestUpdate: { _ in - }, - sendSticker: { [weak controllerInteraction] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets in - guard let controllerInteraction = controllerInteraction else { - return - } - let _ = controllerInteraction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets) + updateSearchQuery: { _ in }, chatPeerId: chatPeerId, peekBehavior: nil, @@ -1329,13 +1325,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { return controllerInteraction?.navigationController() }, requestUpdate: { _ in - }, - sendSticker: { [weak controllerInteraction] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets in - guard let controllerInteraction = controllerInteraction else { - return - } - let _ = controllerInteraction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer, bubbleUpEmojiOrStickersets) + updateSearchQuery: { _ in }, chatPeerId: chatPeerId, peekBehavior: stickerPeekBehavior, @@ -1592,6 +1583,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { strongSelf.hideInputUpdated?(transition.containedViewLayoutTransition) } }, + hideTopPanelUpdated: { _, _ in + }, switchToTextInput: { [weak self] in self?.switchToTextInput?() }, @@ -1721,9 +1714,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode { private func processInputData(inputData: InputData) -> InputData { return InputData( - emoji: inputData.emoji.withUpdatedItemGroups(self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.itemGroups)), + emoji: inputData.emoji.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.itemGroups), itemContentUniqueId: nil), stickers: inputData.stickers.flatMap { stickers in - return stickers.withUpdatedItemGroups(self.processStableItemGroupList(category: .stickers, itemGroups: stickers.itemGroups)) + return stickers.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.itemGroups), itemContentUniqueId: nil) }, gifs: inputData.gifs, availableGifSearchEmojies: inputData.availableGifSearchEmojies @@ -2057,10 +2050,10 @@ final class EntityInputView: UIView, AttachmentTextInputPanelInputView, UIInputV navigationController: { return nil }, - requestUpdate: { _ in - + requestUpdate: { _ in + }, + updateSearchQuery: { _ in }, - sendSticker: nil, chatPeerId: nil, peekBehavior: nil, customLayout: nil, diff --git a/submodules/TelegramUI/Sources/ChatTitleView.swift b/submodules/TelegramUI/Sources/ChatTitleView.swift index c49d65a7fe..75d5c7f006 100644 --- a/submodules/TelegramUI/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Sources/ChatTitleView.swift @@ -673,7 +673,7 @@ final class ChatTitleView: UIView, NavigationBarTitleView { case .premium: titleCredibilityContent = .premium(color: self.theme.list.itemAccentColor) case .verified: - titleCredibilityContent = .verified(fillColor: self.theme.list.itemCheckColors.fillColor, foregroundColor: self.theme.list.itemCheckColors.foregroundColor) + titleCredibilityContent = .verified(fillColor: self.theme.list.itemCheckColors.fillColor, foregroundColor: self.theme.list.itemCheckColors.foregroundColor, sizeType: .large) case .fake: titleCredibilityContent = .text(color: self.theme.chat.message.incoming.scamColor, string: self.strings.Message_FakeAccount.uppercased()) case .scam: diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift index d8ff7bbfba..ee7bae433d 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoHeaderNode.swift @@ -2343,8 +2343,8 @@ final class PeerInfoHeaderNode: ASDisplayNode { emojiRegularStatusContent = .premium(color: presentationData.theme.list.itemAccentColor) emojiExpandedStatusContent = .premium(color: UIColor(rgb: 0xffffff, alpha: 0.75)) case .verified: - emojiRegularStatusContent = .verified(fillColor: presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: presentationData.theme.list.itemCheckColors.foregroundColor) - emojiExpandedStatusContent = .verified(fillColor: UIColor(rgb: 0xffffff, alpha: 0.75), foregroundColor: .clear) + emojiRegularStatusContent = .verified(fillColor: presentationData.theme.list.itemCheckColors.fillColor, foregroundColor: presentationData.theme.list.itemCheckColors.foregroundColor, sizeType: .large) + emojiExpandedStatusContent = .verified(fillColor: UIColor(rgb: 0xffffff, alpha: 0.75), foregroundColor: .clear, sizeType: .large) case .fake: emojiRegularStatusContent = .text(color: presentationData.theme.chat.message.incoming.scamColor, string: presentationData.strings.Message_FakeAccount.uppercased()) emojiExpandedStatusContent = emojiRegularStatusContent diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h index 08303289d3..46e724a905 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.h @@ -22,6 +22,7 @@ typedef NS_OPTIONS(NSUInteger, UIResponderDisableAutomaticKeyboardHandling) { - (void)internalSetStatusBarStyle:(UIStatusBarStyle)style animated:(BOOL)animated; - (void)internalSetStatusBarHidden:(BOOL)hidden animation:(UIStatusBarAnimation)animation; +- (UIWindow * _Nullable)internalGetKeyboard; @end diff --git a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m index 23d8c4e03d..dc8a2d0455 100644 --- a/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m +++ b/submodules/UIKitRuntimeUtils/Source/UIKitRuntimeUtils/UIViewController+Navigation.m @@ -139,6 +139,12 @@ static bool notyfyingShiftState = false; @end +@protocol UIRemoteKeyboardWindowProtocol + ++ (UIWindow * _Nullable)remoteKeyboardWindowForScreen:(UIScreen * _Nullable)screen create:(BOOL)create; + +@end + @implementation UIViewController (Navigation) + (void)load @@ -155,6 +161,8 @@ static bool notyfyingShiftState = false; [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(presentViewController:animated:completion:) newSelector:@selector(_65087dc8_presentViewController:animated:completion:)]; [RuntimeUtils swizzleInstanceMethodOfClass:[UIViewController class] currentSelector:@selector(setNeedsStatusBarAppearanceUpdate) newSelector:@selector(_65087dc8_setNeedsStatusBarAppearanceUpdate)]; + [RuntimeUtils swizzleClassMethodOfClass:NSClassFromString(@"UIRemoteKeyboardWindow") currentSelector:NSSelectorFromString(@"remoteKeyboardWindowForScreen:create:") newSelector:NSSelectorFromString(@"_65087dc8_remoteKeyboardWindowForScreen:create:")]; + if (@available(iOS 15.0, *)) { [RuntimeUtils swizzleInstanceMethodOfClass:[CADisplayLink class] currentSelector:@selector(setPreferredFrameRateRange:) newSelector:@selector(_65087dc8_setPreferredFrameRateRange:)]; } @@ -291,6 +299,14 @@ static bool notyfyingShiftState = false; #pragma clang diagnostic pop } +- (UIWindow * _Nullable)internalGetKeyboard { + Class windowClass = NSClassFromString(@"UIRemoteKeyboardWindow"); + if (!windowClass) { + return nil; + } + return [(id)windowClass remoteKeyboardWindowForScreen:[UIScreen mainScreen] create:false]; +} + @end @implementation UIView (Navigation)