diff --git a/Telegram/BUILD b/Telegram/BUILD index 74b3ecae94..c6ebd55366 100644 --- a/Telegram/BUILD +++ b/Telegram/BUILD @@ -8,8 +8,6 @@ load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application", "ios_extension", "ios_framework", - "ios_unit_test", - "ios_ui_test", ) load("@build_bazel_rules_apple//apple:resources.bzl", @@ -22,7 +20,6 @@ load("@build_bazel_rules_swift//swift:swift.bzl", load( "@rules_xcodeproj//xcodeproj:defs.bzl", - "top_level_target", "top_level_targets", "xcodeproj", "xcode_provisioning_profile", diff --git a/submodules/CallListUI/Sources/CallListController.swift b/submodules/CallListUI/Sources/CallListController.swift index b8e54aecd3..06bfd50c46 100644 --- a/submodules/CallListUI/Sources/CallListController.swift +++ b/submodules/CallListUI/Sources/CallListController.swift @@ -155,6 +155,8 @@ public final class CallListController: TelegramBaseController { if case .navigation = self.mode { self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) } + + self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index a28100bbf7..47492d7859 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -118,6 +118,7 @@ swift_library( "//submodules/TelegramUI/Components/ButtonComponent", "//submodules/TelegramUI/Components/AnimatedTextComponent", "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 5c5e8f23e3..078854b0e6 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -55,6 +55,8 @@ import TextFormat import AvatarUploadToastScreen import AdsInfoScreen import AdsReportScreen +import SearchBarNode +import ChatListFilterTabContainerNode private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController @@ -768,6 +770,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) self.updateNavigationMetadata() + + self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) } required public init(coder aDecoder: NSCoder) { @@ -2838,14 +2842,9 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController func updateHeaderContent() -> (primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?) { var primaryContent: ChatListHeaderComponent.Content? if let primaryContext = self.primaryContext { - var backTitle: String? - if let previousItem = self.previousItem { - switch previousItem { - case let .item(item): - backTitle = item.title ?? self.presentationData.strings.Common_Back - case .close: - backTitle = self.presentationData.strings.Common_Close - } + var displayBackButton: Bool = false + if self.previousItem != nil { + displayBackButton = true } var navigationBackTitle: String? if case .chatList(.archive) = self.location { @@ -2858,8 +2857,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatListTitle: primaryContext.chatListTitle, leftButton: primaryContext.leftButton, rightButtons: primaryContext.rightButtons, - backTitle: backTitle, - backPressed: backTitle != nil ? { [weak self] in + backPressed: displayBackButton ? { [weak self] in guard let self else { return } @@ -2876,7 +2874,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatListTitle: secondaryContext.chatListTitle, leftButton: secondaryContext.leftButton, rightButtons: secondaryContext.rightButtons, - backTitle: nil, backPressed: { [weak self] in guard let self else { return @@ -4638,9 +4635,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController searchContentNode = navigationBarView.searchContentNode } - if let searchContentNode { - self.activateSearch(filter: filter, query: query, skipScrolling: false, searchContentNode: searchContentNode) - } + self.activateSearch(filter: filter, query: query, skipScrolling: false, searchContentNode: searchContentNode) } public func activateSearch(query: String? = nil) { @@ -4654,45 +4649,37 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } private var previousSearchToggleTimestamp: Double? - func activateSearch(filter: ChatListSearchFilter = .chats, query: String? = nil, skipScrolling: Bool = false, searchContentNode: NavigationBarSearchContentNode) { - let currentTimestamp = CACurrentMediaTime() - if let previousSearchActivationTimestamp = self.previousSearchToggleTimestamp, currentTimestamp < previousSearchActivationTimestamp + 0.6 { - return - } - self.previousSearchToggleTimestamp = currentTimestamp - - if let storyTooltip = self.storyTooltip { - storyTooltip.dismiss() - } - - var filter = filter - if case .forum = self.chatListDisplayNode.effectiveContainerNode.location { - filter = .topics - } - - if self.chatListDisplayNode.searchDisplayController == nil { - /*if !skipScrolling, let searchContentNode = self.searchContentNode, searchContentNode.expansionProgress != 1.0 { - self.scrollToTop?() - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2, execute: { [weak self] in - self?.activateSearch(filter: filter, query: query, skipScrolling: true) - }) + func activateSearch(filter: ChatListSearchFilter = .chats, query: String? = nil, skipScrolling: Bool = false, searchContentNode: NavigationBarSearchContentNode?) { + Task { @MainActor [weak self] in + guard let self else { return - }*/ - //TODO:scroll to top? + } + + let currentTimestamp = CACurrentMediaTime() + if let previousSearchActivationTimestamp = self.previousSearchToggleTimestamp, currentTimestamp < previousSearchActivationTimestamp + 0.6 { + return + } + self.previousSearchToggleTimestamp = currentTimestamp - let _ = (combineLatest(self.chatListDisplayNode.mainContainerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1)) - |> deliverOnMainQueue).startStandalone(next: { [weak self] _, chatListView in - Task { @MainActor in - guard let strongSelf = self else { - return - } - + if let storyTooltip = self.storyTooltip { + storyTooltip.dismiss() + } + + var filter = filter + if case .forum = self.chatListDisplayNode.effectiveContainerNode.location { + filter = .topics + } + + if self.chatListDisplayNode.searchDisplayController == nil { + let (_, _) = await combineLatest(self.chatListDisplayNode.mainContainerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1)).get() + + do { /*if let scrollToTop = strongSelf.scrollToTop { - scrollToTop() - }*/ + scrollToTop() + }*/ let tabsIsEmpty: Bool - if let (resolvedItems, displayTabsAtBottom, _) = strongSelf.tabContainerData { + if let (resolvedItems, displayTabsAtBottom, _) = self.tabContainerData { tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom } else { tabsIsEmpty = true @@ -4702,40 +4689,43 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let displaySearchFilters = true - if let filterContainerNodeAndActivate = await strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: strongSelf.hasDownloads, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) { + if let filterContainerNodeAndActivate = await self.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode?.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: self.hasDownloads, initialFilter: filter, navigationController: self.navigationController as? NavigationController, searchBarIsExternal: true) { let (filterContainerNode, activate) = filterContainerNodeAndActivate if displaySearchFilters { let searchTabsNode = SparseNode() - strongSelf.searchTabsNode = searchTabsNode + self.searchTabsNode = searchTabsNode searchTabsNode.addSubnode(filterContainerNode) } activate(filter != .downloads) - if let searchContentNode = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { + if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { searchContentNode.search(filter: filter, query: query) } } let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring) - strongSelf.setDisplayNavigationBar(false, transition: transition) - - (strongSelf.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.4, curve: .spring)) - } - }) - - self.isSearchActive = true - if let navigationController = self.navigationController as? NavigationController { - for controller in navigationController.globalOverlayControllers { - if let controller = controller as? VoiceChatOverlayController { - controller.updateVisibility() - break + self.setDisplayNavigationBar(false, transition: transition) + self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: true), transition: transition) + if let searchBarNode = self.currentTabBarSearchNode?() as? SearchBarNode { + self.chatListDisplayNode.searchDisplayController?.setSearchBar(searchBarNode) + searchBarNode.activate() + } + + self.isSearchActive = true + if let navigationController = self.navigationController as? NavigationController { + for controller in navigationController.globalOverlayControllers { + if let controller = controller as? VoiceChatOverlayController { + controller.updateVisibility() + break + } + } } } - } - } else if self.isSearchActive { - if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { - searchContentNode.search(filter: filter, query: query) + } else if self.isSearchActive { + if let searchContentNode = self.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { + searchContentNode.search(filter: filter, query: query) + } } } } @@ -4766,6 +4756,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } completion = self.chatListDisplayNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode, animated: animated) searchContentNode.placeholderNode.frame = previousFrame + } else { + completion = self.chatListDisplayNode.deactivateSearch(placeholderNode: nil, animated: animated) } self.chatListDisplayNode.tempAllowAvatarExpansion = true @@ -4780,7 +4772,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController completion?() - (self.parent as? TabBarController)?.updateIsTabBarHidden(false, transition: .animated(duration: 0.4, curve: .spring)) + self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: transition) self.isSearchActive = false if let navigationController = self.navigationController as? NavigationController { @@ -6236,6 +6228,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController strongSelf.context.sharedContext.mainWindow?.presentInGlobalOverlay(controller) }) } + + override public func tabBarActivateSearch() { + self.activateSearch() + } + + override public func tabBarDeactivateSearch() { + self.deactivateSearch(animated: true) + } private var playedSignUpCompletedAnimation = false public func playSignUpCompletedAnimation() { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 3a8357317e..9b4a72354e 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -22,6 +22,7 @@ import ChatListHeaderComponent import StoryPeerListComponent import TelegramNotices import EdgeEffect +import ChatListFilterTabContainerNode public enum ChatListContainerNodeFilter: Equatable { case all @@ -1382,8 +1383,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { strings: self.presentationData.strings, statusBarHeight: layout.statusBarHeight ?? 0.0, sideInset: layout.safeInsets.left, + search: nil, //ChatListNavigationBar.Search(isEnabled: true), isSearchActive: self.isSearchDisplayControllerActive, - isSearchEnabled: true, primaryContent: headerContent?.primaryContent, secondaryContent: headerContent?.secondaryContent, secondaryTransition: self.inlineStackContainerTransitionFraction, @@ -1540,7 +1541,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var storiesInset = storiesInset let navigationBarLayout = self.updateNavigationBar(layout: layout, deferScrollApplication: true, transition: ComponentTransition(transition)) - self.mainContainerNode.initialScrollingOffset = ChatListNavigationBar.searchScrollHeight + navigationBarLayout.storiesInset + //self.mainContainerNode.initialScrollingOffset = ChatListNavigationBar.searchScrollHeight + navigationBarLayout.storiesInset + self.mainContainerNode.initialScrollingOffset = navigationBarLayout.storiesInset navigationBarHeight = navigationBarLayout.navigationHeight visualNavigationHeight = navigationBarLayout.navigationHeight @@ -1666,7 +1668,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { } @MainActor - func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) async -> (ASDisplayNode, (Bool) -> Void)? { + func activateSearch(placeholderNode: SearchBarPlaceholderNode?, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?, searchBarIsExternal: Bool) async -> (ASDisplayNode, (Bool) -> Void)? { guard let (containerLayout, _, _, cleanNavigationBarHeight, _) = self.containerLayout, self.searchDisplayController == nil else { return nil } @@ -1712,7 +1714,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() } - }) + }, searchBarIsExternal: searchBarIsExternal) self.mainContainerNode.accessibilityElementsHidden = true self.inlineStackContainerNode?.accessibilityElementsHidden = true @@ -1742,7 +1744,7 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { }) } - func deactivateSearch(placeholderNode: SearchBarPlaceholderNode, animated: Bool) -> (() -> Void)? { + func deactivateSearch(placeholderNode: SearchBarPlaceholderNode?, animated: Bool) -> (() -> Void)? { if let searchDisplayController = self.searchDisplayController { self.isSearchDisplayControllerActive = false self.searchDisplayController = nil @@ -1750,7 +1752,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { self.inlineStackContainerNode?.accessibilityElementsHidden = false return { [weak self, weak placeholderNode] in - if let strongSelf = self, let placeholderNode, let (layout, _, _, cleanNavigationBarHeight, _) = strongSelf.containerLayout { + if let strongSelf = self, let (layout, _, _, cleanNavigationBarHeight, _) = strongSelf.containerLayout { + let placeholderNode = placeholderNode searchDisplayController.deactivate(placeholder: placeholderNode, animated: animated) searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: .animated(duration: 0.4, curve: .spring)) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 46974fd940..9b445ccc3f 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -2103,7 +2103,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let effectiveBackgroundColor: UIColor if item.isPinned { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: self.onlineIsVoiceChat) - effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + //TODO:localize + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor + //effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: self.onlineIsVoiceChat) effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor @@ -2117,7 +2119,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { if let avatarLiveBadge = self.avatarLiveBadge { let effectiveBackgroundColor: UIColor if item.isPinned { - effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + //TODO:localize + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor + //effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor } @@ -4127,7 +4131,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { let effectiveBackgroundColor: UIColor if item.isPinned { - effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + //TODO:localize + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor + //effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor } @@ -4148,7 +4154,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { effectiveBackgroundColor = item.presentationData.theme.chatList.itemHighlightedBackgroundColor } else if case let .chatList(index) = item.index, index.pinningIndex != nil { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: onlineIsVoiceChat) - effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor + //TODO:localize + effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor + //effectiveBackgroundColor = item.presentationData.theme.chatList.pinnedItemBackgroundColor } else { onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: onlineIsVoiceChat) effectiveBackgroundColor = item.presentationData.theme.chatList.itemBackgroundColor @@ -5060,7 +5068,9 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode { backgroundColor = theme.itemBackgroundColor highlightedBackgroundColor = theme.itemHighlightedBackgroundColor } else { - backgroundColor = theme.pinnedItemBackgroundColor + //TODO:localize + backgroundColor = item.presentationData.theme.chatList.itemBackgroundColor + //backgroundColor = theme.pinnedItemBackgroundColor highlightedBackgroundColor = theme.pinnedItemHighlightedBackgroundColor } } else { diff --git a/submodules/ComponentFlow/Source/Base/Transition.swift b/submodules/ComponentFlow/Source/Base/Transition.swift index 4dcd0401d2..ae393778c4 100644 --- a/submodules/ComponentFlow/Source/Base/Transition.swift +++ b/submodules/ComponentFlow/Source/Base/Transition.swift @@ -1335,6 +1335,41 @@ public struct ComponentTransition { completion: completion ) } + + public func setBlur(layer: CALayer, radius: CGFloat, completion: ((Bool) -> Void)? = nil) { + var currentRadius: CGFloat = 0.0 + if let currentFilters = layer.filters { + for filter in currentFilters { + if let filter = filter as? NSObject, filter.description.contains("gaussianBlur") { + currentRadius = filter.value(forKey: "inputRadius") as? CGFloat ?? 0.0 + } + } + } + + if currentRadius == radius { + completion?(true) + return + } + + if let blurFilter = CALayer.blur() { + blurFilter.setValue(radius as NSNumber, forKey: "inputRadius") + layer.filters = [blurFilter] + switch self.animation { + case .none: + completion?(true) + case let .curve(duration, curve): + layer.animate(from: currentRadius as NSNumber, to: radius as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", duration: duration, delay: 0.0, curve: curve, removeOnCompletion: true, additive: false,completion: { [weak layer] flag in + if let layer { + if radius <= 0.0 { + layer.filters = nil + } + } + + completion?(flag) + }) + } + } + } public func animateBlur(layer: CALayer, fromRadius: CGFloat, toRadius: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { let duration: Double diff --git a/submodules/ContactListUI/Sources/ContactsController.swift b/submodules/ContactListUI/Sources/ContactsController.swift index 425ca2b7ab..e75897f583 100644 --- a/submodules/ContactListUI/Sources/ContactsController.swift +++ b/submodules/ContactListUI/Sources/ContactsController.swift @@ -226,6 +226,8 @@ public class ContactsController: ViewController { } self.sortButton.addTarget(self, action: #selector(self.sortPressed), forControlEvents: .touchUpInside) + + self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index 2718f81f91..bd1e967a3b 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -350,7 +350,6 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { chatListTitle: NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: nil), leftButton: leftButton, rightButtons: rightButtons, - backTitle: nil, backPressed: nil ) @@ -362,8 +361,8 @@ final class ContactsControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { strings: self.presentationData.strings, statusBarHeight: layout.statusBarHeight ?? 0.0, sideInset: layout.safeInsets.left, + search: ChatListNavigationBar.Search(isEnabled: true), isSearchActive: self.isSearchDisplayControllerActive, - isSearchEnabled: true, primaryContent: primaryContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 593005a7e5..f81403f2dd 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -217,6 +217,18 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { open var navigationBarRequiresEntireLayoutUpdate: Bool { return true } + + public struct TabBarSearchState: Equatable { + public var isActive: Bool + + public init(isActive: Bool) { + self.isActive = isActive + } + } + + public private(set) var tabBarSearchState: TabBarSearchState? + public var tabBarSearchStateUpdated: ((ContainedViewLayoutTransition) -> Void)? + public var currentTabBarSearchNode: (() -> ASDisplayNode?)? private weak var activeInputViewCandidate: UIResponder? private weak var activeInputView: UIResponder? @@ -705,9 +717,22 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { open func tabBarDisabledAction() { } + + open func tabBarActivateSearch() { + } + + open func tabBarDeactivateSearch() { + } open func tabBarItemSwipeAction(direction: TabBarItemSwipeDirection) { } + + public func updateTabBarSearchState(_ tabBarSearchState: TabBarSearchState?, transition: ContainedViewLayoutTransition) { + if self.tabBarSearchState != tabBarSearchState { + self.tabBarSearchState = tabBarSearchState + self.tabBarSearchStateUpdated?(transition) + } + } open func updatePossibleControllerDropContent(content: NavigationControllerDropContent?) { } diff --git a/submodules/SearchBarNode/Sources/SearchBarNode.swift b/submodules/SearchBarNode/Sources/SearchBarNode.swift index 131323d318..773e1f4259 100644 --- a/submodules/SearchBarNode/Sources/SearchBarNode.swift +++ b/submodules/SearchBarNode/Sources/SearchBarNode.swift @@ -369,6 +369,7 @@ private class SearchBarTextField: UITextField, UIScrollViewDelegate { } var theme: SearchBarNodeTheme + let style: SearchBarStyle fileprivate func layoutTokens(transition: ContainedViewLayoutTransition = .immediate) { var hasSelected = false @@ -519,8 +520,9 @@ private class SearchBarTextField: UITextField, UIScrollViewDelegate { } } - init(theme: SearchBarNodeTheme) { + init(theme: SearchBarNodeTheme, style: SearchBarStyle) { self.theme = theme + self.style = style self.placeholderLabel = ImmediateTextNode() self.placeholderLabel.isUserInteractionEnabled = false @@ -679,6 +681,10 @@ private class SearchBarTextField: UITextField, UIScrollViewDelegate { placeholderYOffset = 1.0 } else { } + + if case .inlineNavigation = self.style { + placeholderYOffset += 6.0 + } let textRect = self.textRect(forBounds: bounds) let labelSize = self.placeholderLabel.updateLayout(textRect.size) @@ -813,40 +819,45 @@ public final class SearchBarNodeTheme: Equatable { public enum SearchBarStyle { case modern case legacy + case inlineNavigation var font: UIFont { switch self { - case .modern: - return Font.regular(17.0) - case .legacy: - return Font.regular(14.0) + case .modern, .inlineNavigation: + return Font.regular(17.0) + case .legacy: + return Font.regular(14.0) } } var cornerDiameter: CGFloat { switch self { - case .modern: - return 21.0 - case .legacy: - return 14.0 + case .modern, .inlineNavigation: + return 21.0 + case .legacy: + return 14.0 } } var height: CGFloat { switch self { - case .modern: - return 36.0 - case .legacy: - return 28.0 + case .inlineNavigation: + return 48.0 + case .modern: + return 36.0 + case .legacy: + return 28.0 } } var padding: CGFloat { switch self { - case .modern: - return 10.0 - case .legacy: - return 8.0 + case .inlineNavigation: + return 0.0 + case .modern: + return 10.0 + case .legacy: + return 8.0 } } } @@ -994,7 +1005,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true - self.textField = SearchBarTextField(theme: theme) + self.textField = SearchBarTextField(theme: theme, style: fieldStyle) self.textField.accessibilityTraits = .searchField self.textField.autocorrectionType = .no self.textField.returnKeyType = .search @@ -1011,14 +1022,21 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { super.init() - self.addSubnode(self.backgroundNode) - self.addSubnode(self.separatorNode) - - self.addSubnode(self.textBackgroundNode) + if case .inlineNavigation = self.fieldStyle { + } else { + self.addSubnode(self.backgroundNode) + self.addSubnode(self.separatorNode) + self.addSubnode(self.textBackgroundNode) + } self.view.addSubview(self.textField) - self.addSubnode(self.iconNode) + + if case .inlineNavigation = self.fieldStyle { + } else { + self.addSubnode(self.iconNode) + self.addSubnode(self.cancelButton) + } + self.addSubnode(self.clearButton) - self.addSubnode(self.cancelButton) self.textField.delegate = self self.textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged) @@ -1093,19 +1111,34 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: .immediate) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel))) - let verticalOffset: CGFloat = boundingSize.height - 82.0 - let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: boundingSize.width - leftInset - rightInset, height: boundingSize.height)) - let textBackgroundHeight = self.fieldStyle.height + let textBackgroundHeight: CGFloat + if case .inlineNavigation = self.fieldStyle { + textBackgroundHeight = boundingSize.height + } else { + textBackgroundHeight = self.fieldStyle.height + } + let verticalOffset: CGFloat + if case .inlineNavigation = self.fieldStyle { + verticalOffset = -textBackgroundHeight + } else { + verticalOffset = boundingSize.height - 82.0 + } let cancelButtonSize = self.cancelButton.measure(CGSize(width: 100.0, height: CGFloat.infinity)) transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: contentFrame.maxX - 10.0 - cancelButtonSize.width, y: verticalOffset + textBackgroundHeight + floorToScreenPixels((textBackgroundHeight - cancelButtonSize.height) / 2.0)), size: cancelButtonSize)) let padding = self.fieldStyle.padding - let textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + padding, y: verticalOffset + textBackgroundHeight), size: CGSize(width: contentFrame.width - padding * 2.0 - (self.hasCancelButton ? cancelButtonSize.width + 11.0 : 0.0), height: textBackgroundHeight)) + var textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + padding, y: verticalOffset + textBackgroundHeight), size: CGSize(width: contentFrame.width - padding * 2.0 - (self.hasCancelButton ? cancelButtonSize.width + 11.0 : 0.0), height: textBackgroundHeight)) transition.updateFrame(node: self.textBackgroundNode, frame: textBackgroundFrame) - let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 24.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 24.0 - 27.0), height: textBackgroundFrame.size.height)) + var textFrame = CGRect(origin: CGPoint(x: 0.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 24.0 - 27.0), height: textBackgroundFrame.size.height)) + if case .inlineNavigation = self.fieldStyle { + textFrame.size.width = boundingSize.width - 27.0 + textBackgroundFrame.size.width = boundingSize.width + } else { + textFrame.origin.x = textBackgroundFrame.minX + 24.0 + } if let iconImage = self.iconNode.image { let iconSize = iconImage.size diff --git a/submodules/SearchUI/Sources/SearchDisplayController.swift b/submodules/SearchUI/Sources/SearchDisplayController.swift index 50420e6b50..3c885e33e0 100644 --- a/submodules/SearchUI/Sources/SearchDisplayController.swift +++ b/submodules/SearchUI/Sources/SearchDisplayController.swift @@ -25,12 +25,14 @@ public final class SearchDisplayController { } } - private let searchBar: SearchBarNode + private var searchBar: SearchBarNode? + private let searchBarIsExternal: Bool private let mode: SearchDisplayControllerMode private let backgroundNode: BackgroundNode public let contentNode: SearchDisplayControllerContentNode private var hasSeparator: Bool private let inline: Bool + private let cancel: () -> Void private var containerLayout: (ContainerViewLayout, CGFloat)? @@ -38,71 +40,75 @@ public final class SearchDisplayController { private var isSearchingDisposable: Disposable? - public init(presentationData: PresentationData, mode: SearchDisplayControllerMode = .navigation, placeholder: String? = nil, hasBackground: Bool = false, hasSeparator: Bool = false, contentNode: SearchDisplayControllerContentNode, inline: Bool = false, cancel: @escaping () -> Void) { + public init( + presentationData: PresentationData, + mode: SearchDisplayControllerMode = .navigation, + placeholder: String? = nil, + hasBackground: Bool = false, + hasSeparator: Bool = false, + contentNode: SearchDisplayControllerContentNode, + inline: Bool = false, + cancel: @escaping () -> Void, + fieldStyle: SearchBarStyle = .modern, + searchBarIsExternal: Bool = false + ) { self.inline = inline - self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasBackground: hasBackground, hasSeparator: hasSeparator, inline: inline), strings: presentationData.strings, fieldStyle: .modern, forceSeparator: hasSeparator, displayBackground: hasBackground) + self.cancel = cancel + self.searchBarIsExternal = searchBarIsExternal + + if !searchBarIsExternal { + self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: presentationData.theme, hasBackground: hasBackground, hasSeparator: hasSeparator, inline: inline), strings: presentationData.strings, fieldStyle: fieldStyle, forceSeparator: hasSeparator, displayBackground: hasBackground) + } self.backgroundNode = BackgroundNode() self.backgroundNode.allowsGroupOpacity = true self.mode = mode self.contentNode = contentNode self.hasSeparator = hasSeparator + + if let searchBar = self.searchBar { + self.setSearchBar(searchBar) + } - self.searchBar.textUpdated = { [weak contentNode] text, _ in - contentNode?.searchTextUpdated(text: text) - } - self.searchBar.tokensUpdated = { [weak contentNode] tokens in - contentNode?.searchTokensUpdated(tokens: tokens) - } - self.searchBar.cancel = { [weak self] in - self?.isDeactivating = true - cancel() - } - self.searchBar.clearPrefix = { [weak contentNode] in - contentNode?.searchTextClearPrefix() - } - self.searchBar.clearTokens = { [weak contentNode] in - contentNode?.searchTextClearTokens() - } self.contentNode.cancel = { [weak self] in self?.isDeactivating = true cancel() } self.contentNode.dismissInput = { [weak self] in - self?.searchBar.deactivate(clear: false) + self?.searchBar?.deactivate(clear: false) } var isFirstTime = true self.contentNode.setQuery = { [weak self] prefix, tokens, query in - if let strongSelf = self { - strongSelf.searchBar.prefixString = prefix - let previousTokens = strongSelf.searchBar.tokens - strongSelf.searchBar.tokens = tokens - strongSelf.searchBar.text = query + if let strongSelf = self, let searchBar = strongSelf.searchBar { + searchBar.prefixString = prefix + let previousTokens = searchBar.tokens + searchBar.tokens = tokens + searchBar.text = query if previousTokens.count < tokens.count && !isFirstTime { if let lastToken = tokens.last, !lastToken.permanent { - strongSelf.searchBar.selectLastToken() + searchBar.selectLastToken() } } isFirstTime = false } } if let placeholder = placeholder { - self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor) + self.searchBar?.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: presentationData.theme.rootController.navigationSearchBar.inputPlaceholderTextColor) } self.contentNode.setPlaceholder = { [weak self] string in - guard string != self?.searchBar.placeholderString?.string else { + guard string != self?.searchBar?.placeholderString?.string else { return } - if let mutableAttributedString = self?.searchBar.placeholderString?.mutableCopy() as? NSMutableAttributedString { + if let mutableAttributedString = self?.searchBar?.placeholderString?.mutableCopy() as? NSMutableAttributedString { mutableAttributedString.mutableString.setString(string) - self?.searchBar.placeholderString = mutableAttributedString + self?.searchBar?.placeholderString = mutableAttributedString } } self.isSearchingDisposable = (contentNode.isSearching |> deliverOnMainQueue).start(next: { [weak self] value in - self?.searchBar.activity = value + self?.searchBar?.activity = value }) if self.contentNode.hasDim { @@ -113,9 +119,32 @@ public final class SearchDisplayController { self.backgroundNode.isTransparent = false } } + + public func setSearchBar(_ searchBar: SearchBarNode) { + self.searchBar = searchBar + + searchBar.textUpdated = { [weak contentNode] text, _ in + contentNode?.searchTextUpdated(text: text) + } + searchBar.tokensUpdated = { [weak contentNode] tokens in + contentNode?.searchTokensUpdated(tokens: tokens) + } + searchBar.cancel = { [weak self] in + self?.isDeactivating = true + self?.cancel() + } + searchBar.clearPrefix = { [weak contentNode] in + contentNode?.searchTextClearPrefix() + } + searchBar.clearTokens = { [weak contentNode] in + contentNode?.searchTextClearTokens() + } + } public func updatePresentationData(_ presentationData: PresentationData) { - self.searchBar.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: self.hasSeparator, inline: self.inline), strings: presentationData.strings) + if !self.searchBarIsExternal { + self.searchBar?.updateThemeAndStrings(theme: SearchBarNodeTheme(theme: presentationData.theme, hasSeparator: self.hasSeparator, inline: self.inline), strings: presentationData.strings) + } self.contentNode.updatePresentationData(presentationData) if self.contentNode.hasDim { @@ -137,21 +166,27 @@ public final class SearchDisplayController { navigationBarOffset = 0.0 } var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: searchBarHeight)) + if self.searchBarIsExternal { + navigationBarFrame.size.height -= 10.0 + } if layout.statusBarHeight == nil { navigationBarFrame.size.height = 64.0 } navigationBarFrame.size.height += 10.0 + let navigationBarHeight = navigationBarFrame.maxY - let searchBarFrame: CGRect - if case .navigation = self.mode { - searchBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0) - } else { - searchBarFrame = navigationBarFrame + if !self.searchBarIsExternal, let searchBar = self.searchBar { + let searchBarFrame: CGRect + if case .navigation = self.mode { + searchBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0) + } else { + searchBarFrame = navigationBarFrame + } + transition.updateFrame(node: searchBar, frame: searchBarFrame) + searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) } - transition.updateFrame(node: self.searchBar, frame: searchBarFrame) - self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition) - self.containerLayout = (layout, navigationBarFrame.maxY) + self.containerLayout = (layout, navigationBarHeight) let bounds = CGRect(origin: CGPoint(), size: layout.size) transition.updateFrame(node: self.backgroundNode, frame: bounds.insetBy(dx: -20.0, dy: -20.0)) @@ -205,18 +240,20 @@ public final class SearchDisplayController { self.backgroundNode.layer.animateScale(from: 0.85, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } - if !self.contentNode.hasDim { - if let placeholder = placeholder { - self.searchBar.placeholderString = placeholder.placeholderString - } - } else { - if let placeholder = placeholder { - let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil) - let contentNodePosition = self.backgroundNode.layer.position - if contentNode.animateBackgroundAppearance { - self.backgroundNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + if !self.searchBarIsExternal { + if !self.contentNode.hasDim { + if let placeholder = placeholder { + self.searchBar?.placeholderString = placeholder.placeholderString + } + } else { + if let placeholder = placeholder { + let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil) + let contentNodePosition = self.backgroundNode.layer.position + if contentNode.animateBackgroundAppearance { + self.backgroundNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - contentNavigationBarHeight)), to: contentNodePosition, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + self.searchBar?.placeholderString = placeholder.placeholderString } - self.searchBar.placeholderString = placeholder.placeholderString } } @@ -240,39 +277,49 @@ public final class SearchDisplayController { navigationBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0) } - self.searchBar.frame = navigationBarFrame - insertSubnode(self.searchBar, true) - self.searchBar.layout() - - if focus { - self.searchBar.activate() + if !self.searchBarIsExternal, let searchBar = self.searchBar { + searchBar.frame = navigationBarFrame + insertSubnode(searchBar, true) + searchBar.layout() + + if focus { + searchBar.activate() + } } + if let placeholder = placeholder { - self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + if !self.searchBarIsExternal, let searchBar = self.searchBar { + searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } if self.contentNode.hasDim { self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) } } else { - self.searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + if !self.searchBarIsExternal, let searchBar = self.searchBar { + searchBar.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + } self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) } } public func deactivate(placeholder: SearchBarPlaceholderNode?, animated: Bool = true) { - self.searchBar.deactivate(clear: false) + if let searchBar = self.searchBar { + searchBar.deactivate(clear: false) + } - let searchBar = self.searchBar - if let placeholder = placeholder { - searchBar.transitionOut(to: placeholder, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: { - [weak searchBar] in - searchBar?.removeFromSupernode() - }) - } else { - searchBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchBar] finished in - if finished { + if !self.searchBarIsExternal, let searchBar = self.searchBar { + if let placeholder = placeholder { + searchBar.transitionOut(to: placeholder, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: { + [weak searchBar] in searchBar?.removeFromSupernode() - } - }) + }) + } else { + searchBar.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak searchBar] finished in + if finished { + searchBar?.removeFromSupernode() + } + }) + } } let backgroundNode = self.backgroundNode diff --git a/submodules/TabBarUI/Sources/TabBarContollerNode.swift b/submodules/TabBarUI/Sources/TabBarContollerNode.swift index abee862105..c346769805 100644 --- a/submodules/TabBarUI/Sources/TabBarContollerNode.swift +++ b/submodules/TabBarUI/Sources/TabBarContollerNode.swift @@ -18,15 +18,18 @@ final class TabBarControllerNode: ASDisplayNode { let layout: ContainerViewLayout let toolbar: Toolbar? let isTabBarHidden: Bool + let currentControllerSearchState: ViewController.TabBarSearchState? init( layout: ContainerViewLayout, toolbar: Toolbar?, - isTabBarHidden: Bool + isTabBarHidden: Bool, + currentControllerSearchState: ViewController.TabBarSearchState? ) { self.layout = layout self.toolbar = toolbar self.isTabBarHidden = isTabBarHidden + self.currentControllerSearchState = currentControllerSearchState } } @@ -51,6 +54,7 @@ final class TabBarControllerNode: ASDisplayNode { } private var theme: PresentationTheme + private var strings: PresentationStrings private let itemSelected: (Int, Bool, [ASDisplayNode]) -> Void private let contextAction: (Int, ContextExtractedContentContainingView, ContextGesture) -> Void @@ -60,24 +64,26 @@ final class TabBarControllerNode: ASDisplayNode { private var toolbarNode: ToolbarNode? private let toolbarActionSelected: (ToolbarActionOption) -> Void private let disabledPressed: () -> Void + private let activateSearch: () -> Void + private let deactivateSearch: () -> Void private(set) var tabBarItems: [TabBarNodeItem] = [] private(set) var selectedIndex: Int = 0 - private(set) var currentControllerNode: ASDisplayNode? + private weak var currentController: ViewController? private var layoutResult: LayoutResult? private var isUpdateRequested: Bool = false private var isChangingSelectedIndex: Bool = false - func setCurrentControllerNode(_ node: ASDisplayNode?) -> () -> Void { - guard node !== self.currentControllerNode else { + func setCurrentController(_ controller: ViewController?) -> () -> Void { + guard controller !== self.currentController else { return {} } - let previousNode = self.currentControllerNode - self.currentControllerNode = node - if let currentControllerNode = self.currentControllerNode { + let previousNode = self.currentController?.displayNode + self.currentController = controller + if let currentControllerNode = self.currentController?.displayNode { if let previousNode { self.insertSubnode(currentControllerNode, aboveSubnode: previousNode) } else { @@ -89,14 +95,22 @@ final class TabBarControllerNode: ASDisplayNode { } return { [weak self, weak previousNode] in - if previousNode !== self?.currentControllerNode { + if previousNode !== self?.currentController?.displayNode { previousNode?.removeFromSupernode() } } } + + var currentSearchNode: ASDisplayNode? { + if let tabBarComponentView = self.tabBarView.view as? TabBarComponent.View { + return tabBarComponentView.currentSearchNode + } + return nil + } - init(theme: PresentationTheme, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingView, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void) { + init(theme: PresentationTheme, strings: PresentationStrings, itemSelected: @escaping (Int, Bool, [ASDisplayNode]) -> Void, contextAction: @escaping (Int, ContextExtractedContentContainingView, ContextGesture) -> Void, swipeAction: @escaping (Int, TabBarItemSwipeDirection) -> Void, toolbarActionSelected: @escaping (ToolbarActionOption) -> Void, disabledPressed: @escaping () -> Void, activateSearch: @escaping () -> Void, deactivateSearch: @escaping () -> Void) { self.theme = theme + self.strings = strings self.itemSelected = itemSelected self.contextAction = contextAction self.disabledOverlayNode = ASDisplayNode() @@ -104,7 +118,9 @@ final class TabBarControllerNode: ASDisplayNode { self.disabledOverlayNode.alpha = 0.0 self.toolbarActionSelected = toolbarActionSelected self.disabledPressed = disabledPressed - + self.activateSearch = activateSearch + self.deactivateSearch = deactivateSearch + super.init() self.setViewBlock({ @@ -163,7 +179,7 @@ final class TabBarControllerNode: ASDisplayNode { } func containerLayoutUpdated(_ layout: ContainerViewLayout, toolbar: Toolbar?, transition: ContainedViewLayoutTransition) -> CGFloat { - let params = Params(layout: layout, toolbar: toolbar, isTabBarHidden: self.tabBarHidden) + let params = Params(layout: layout, toolbar: toolbar, isTabBarHidden: self.tabBarHidden, currentControllerSearchState: self.currentController?.tabBarSearchState) if let layoutResult = self.layoutResult, layoutResult.params == params { return layoutResult.bottomInset } else { @@ -179,18 +195,27 @@ final class TabBarControllerNode: ASDisplayNode { } private func updateImpl(params: Params, transition: ContainedViewLayoutTransition) -> CGFloat { - var options: ContainerViewLayoutInsetOptions = [] - if params.layout.metrics.widthClass == .regular { - options.insert(.input) + var panelsBottomInset: CGFloat = params.layout.insets(options: []).bottom + if params.layout.metrics.widthClass == .regular, let inputHeight = params.layout.inputHeight, inputHeight != 0.0 { + panelsBottomInset = inputHeight + 8.0 } - - var bottomInset: CGFloat = params.layout.insets(options: options).bottom - if bottomInset == 0.0 { - bottomInset = 8.0 + if panelsBottomInset == 0.0 { + panelsBottomInset = 8.0 } else { - bottomInset = max(bottomInset, 8.0) + panelsBottomInset = max(panelsBottomInset, 8.0) + } + + var tabBarBottomInset: CGFloat = panelsBottomInset + if let currentController = self.currentController { + if let tabBarSearchState = currentController.tabBarSearchState, tabBarSearchState.isActive, let inputHeight = params.layout.inputHeight, inputHeight != 0.0 { + tabBarBottomInset = max(tabBarBottomInset, inputHeight + 8.0) + } + } + + var sideInset: CGFloat = 12.0 + if tabBarBottomInset <= 28.0 { + sideInset = 20.0 } - let sideInset: CGFloat = 20.0 var selectedId: AnyHashable? if self.selectedIndex < self.tabBarItems.count { @@ -208,6 +233,7 @@ final class TabBarControllerNode: ASDisplayNode { transition: tabBarTransition, component: AnyComponent(TabBarComponent( theme: self.theme, + strings: self.strings, items: self.tabBarItems.map { item in let itemId = AnyHashable(ObjectIdentifier(item.item)) return TabBarComponent.Item( @@ -230,13 +256,30 @@ final class TabBarControllerNode: ASDisplayNode { } ) }, + search: self.currentController?.tabBarSearchState.flatMap { tabBarSearchState in + return TabBarComponent.Search( + isActive: tabBarSearchState.isActive, + activate: { [weak self] in + guard let self else { + return + } + self.activateSearch() + }, + deactivate: { [weak self] in + guard let self else { + return + } + self.deactivateSearch() + } + ) + }, selectedId: selectedId, isTablet: params.layout.metrics.isTablet )), environment: {}, containerSize: CGSize(width: params.layout.size.width - sideInset * 2.0, height: 100.0) ) - let tabBarFrame = CGRect(origin: CGPoint(x: floor((params.layout.size.width - tabBarSize.width) * 0.5), y: params.layout.size.height - (self.tabBarHidden ? 0.0 : (tabBarSize.height + bottomInset))), size: tabBarSize) + let tabBarFrame = CGRect(origin: CGPoint(x: floor((params.layout.size.width - tabBarSize.width) * 0.5), y: params.layout.size.height - (self.tabBarHidden ? 0.0 : (tabBarSize.height + tabBarBottomInset))), size: tabBarSize) if let tabBarComponentView = self.tabBarView.view { if tabBarComponentView.superview == nil { @@ -248,13 +291,13 @@ final class TabBarControllerNode: ASDisplayNode { transition.updateFrame(node: self.disabledOverlayNode, frame: tabBarFrame) - let toolbarHeight = 50.0 + params.layout.insets(options: options).bottom + let toolbarHeight = 50.0 + panelsBottomInset let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: params.layout.size.height - toolbarHeight), size: CGSize(width: params.layout.size.width, height: toolbarHeight)) if let toolbar = params.toolbar { if let toolbarNode = self.toolbarNode { transition.updateFrame(node: toolbarNode, frame: toolbarFrame) - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: transition) + toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: panelsBottomInset, toolbar: toolbar, transition: transition) } else { let toolbarNode = ToolbarNode(theme: ToolbarTheme(theme: self.theme), displaySeparator: true, left: { [weak self] in self?.toolbarActionSelected(.left) @@ -264,7 +307,7 @@ final class TabBarControllerNode: ASDisplayNode { self?.toolbarActionSelected(.middle) }) toolbarNode.frame = toolbarFrame - toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: .immediate) + toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: params.layout.safeInsets.left, rightInset: params.layout.safeInsets.right, additionalSideInsets: params.layout.additionalInsets, bottomInset: panelsBottomInset, toolbar: toolbar, transition: .immediate) self.addSubnode(toolbarNode) self.toolbarNode = toolbarNode if transition.isAnimated { diff --git a/submodules/TabBarUI/Sources/TabBarController.swift b/submodules/TabBarUI/Sources/TabBarController.swift index a3f7fe0806..111fe208d1 100644 --- a/submodules/TabBarUI/Sources/TabBarController.swift +++ b/submodules/TabBarUI/Sources/TabBarController.swift @@ -93,9 +93,11 @@ open class TabBarControllerImpl: ViewController, TabBarController { private let pendingControllerDisposable = MetaDisposable() private var theme: PresentationTheme + private var strings: PresentationStrings - public init(theme: PresentationTheme) { + public init(theme: PresentationTheme, strings: PresentationStrings) { self.theme = theme + self.strings = strings super.init(navigationBarPresentationData: nil) @@ -152,7 +154,7 @@ open class TabBarControllerImpl: ViewController, TabBarController { } override open func loadDisplayNode() { - self.displayNode = TabBarControllerNode(theme: self.theme, itemSelected: { [weak self] index, longTap, itemNodes in + self.displayNode = TabBarControllerNode(theme: self.theme, strings: self.strings, itemSelected: { [weak self] index, longTap, itemNodes in if let strongSelf = self { if longTap, let controller = strongSelf.controllers[index] as? TabBarContainedController { controller.presentTabBarPreviewingController(sourceNodes: itemNodes) @@ -235,6 +237,16 @@ open class TabBarControllerImpl: ViewController, TabBarController { self?.currentController?.toolbarActionSelected(action: action) }, disabledPressed: { [weak self] in self?.currentController?.tabBarDisabledAction() + }, activateSearch: { [weak self] in + guard let self else { + return + } + self.currentController?.tabBarActivateSearch() + }, deactivateSearch: { [weak self] in + guard let self else { + return + } + self.currentController?.tabBarDeactivateSearch() }) self.updateSelectedIndex() @@ -263,7 +275,8 @@ open class TabBarControllerImpl: ViewController, TabBarController { } if let currentController = self.currentController { currentController.willMove(toParent: nil) - //self.tabBarControllerNode.currentControllerNode = nil + currentController.tabBarSearchStateUpdated = nil + currentController.currentTabBarSearchNode = nil if animated { currentController.view.layer.animateScale(from: 1.0, to: transitionScale, duration: 0.12, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { completed in @@ -286,7 +299,7 @@ open class TabBarControllerImpl: ViewController, TabBarController { currentController.willMove(toParent: self) self.addChild(currentController) - let commit = self.tabBarControllerNode.setCurrentControllerNode(currentController.displayNode) + let commit = self.tabBarControllerNode.setCurrentController(currentController) if animated { currentController.view.layer.animateScale(from: transitionScale, to: 1.0, duration: 0.15, delay: 0.1, timingFunction: kCAMediaTimingFunctionSpring) currentController.view.layer.allowsGroupOpacity = true @@ -303,6 +316,22 @@ open class TabBarControllerImpl: ViewController, TabBarController { currentController.displayNode.recursivelyEnsureDisplaySynchronously(true) self.statusBar.statusBarStyle = currentController.statusBar.statusBarStyle + + currentController.tabBarSearchStateUpdated = { [weak self] transition in + guard let self else { + return + } + if let layout = self.validLayout { + self.containerLayoutUpdated(layout, transition: transition) + } + } + + currentController.currentTabBarSearchNode = { [weak self] in + guard let self else { + return nil + } + return self.tabBarControllerNode.currentSearchNode + } } if let layout = self.validLayout { diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 903663967c..99ef8a56b5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -1174,6 +1174,9 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg self.touchDownGestureRecognizer = recognizer textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string + + self.isAccessibilityContainer = true + self.accessibilityElements = [textInputNode.textView] } private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics, bottomInset: CGFloat) -> CGFloat { diff --git a/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/BUILD b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/BUILD new file mode 100644 index 0000000000..4805b62a3d --- /dev/null +++ b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/BUILD @@ -0,0 +1,26 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatListFilterTabContainerNode", + module_name = "ChatListFilterTabContainerNode", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/TelegramCore", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/TextNodeWithEntities", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", + "//submodules/TelegramUI/Components/LiquidLens", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift similarity index 96% rename from submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift rename to submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift index b20fc85916..9e3fb4dd03 100644 --- a/submodules/ChatListUI/Sources/ChatListFilterTabContainerNode.swift +++ b/submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode/Sources/ChatListFilterTabContainerNode.swift @@ -210,7 +210,7 @@ private final class ItemNode: ASDisplayNode { if self.theme !== presentationData.theme { self.theme = presentationData.theme - self.badgeBackgroundActiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeActiveBackgroundColor) + self.badgeBackgroundActiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chat.inputPanel.panelControlColor) self.badgeBackgroundInactiveNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: presentationData.theme.chatList.unreadBadgeInactiveBackgroundColor) themeUpdated = true @@ -299,10 +299,10 @@ private final class ItemNode: ASDisplayNode { if themeUpdated || titleUpdated { self.titleNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor) - self.titleActiveNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) + self.titleActiveNode.attributedText = title.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.chat.inputPanel.panelControlColor) self.shortTitleNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemSecondaryTextColor) - self.shortTitleActiveNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.list.itemAccentColor) + self.shortTitleActiveNode.attributedText = shortTitle.attributedString(font: Font.medium(14.0), textColor: presentationData.theme.chat.inputPanel.panelControlColor) } if unreadCount != 0 { @@ -438,8 +438,8 @@ public enum ChatListFilterTabEntryId: Hashable { } public struct ChatListFilterTabEntryUnreadCount: Equatable { - let value: Int - let hasUnmuted: Bool + public let value: Int + public let hasUnmuted: Bool public init(value: Int, hasUnmuted: Bool) { self.value = value @@ -482,14 +482,14 @@ public enum ChatListFilterTabEntry: Equatable { public final class ChatListFilterTabContainerNode: ASDisplayNode { private let context: AccountContext private let scrollNode: ASScrollNode - private let selectedLineNode: ASImageNode + private let selectedBackgroundNode: ASImageNode private var itemNodes: [ChatListFilterTabEntryId: ItemNode] = [:] public var tabSelected: ((ChatListFilterTabEntryId, Bool) -> Void)? - var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)? - var addFilter: (() -> Void)? - var contextGesture: ((Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void)? - var presentPremiumTip: (() -> Void)? + public var tabRequestedDeletion: ((ChatListFilterTabEntryId) -> Void)? + public var addFilter: (() -> Void)? + public var contextGesture: ((Int32?, ContextExtractedContentContainingNode, ContextGesture, Bool) -> Void)? + public var presentPremiumTip: (() -> Void)? private var reorderingGesture: ReorderingGestureRecognizer? private var reorderingItem: ChatListFilterTabEntryId? @@ -501,7 +501,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { private var currentParams: (size: CGSize, sideInset: CGFloat, filters: [ChatListFilterTabEntry], selectedFilter: ChatListFilterTabEntryId?, isReordering: Bool, isEditing: Bool, canReorderAllChats: Bool, filtersLimit: Int32?, transitionFraction: CGFloat, presentationData: PresentationData)? - var reorderedFilterIds: [Int32]? { + public var reorderedFilterIds: [Int32]? { return self.reorderedItemIds.flatMap { $0.compactMap { switch $0 { @@ -533,9 +533,9 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { self.context = context self.scrollNode = ASScrollNode() - self.selectedLineNode = ASImageNode() - self.selectedLineNode.displaysAsynchronously = false - self.selectedLineNode.displayWithoutProcessing = true + self.selectedBackgroundNode = ASImageNode() + self.selectedBackgroundNode.displaysAsynchronously = false + self.selectedBackgroundNode.displayWithoutProcessing = true super.init() @@ -549,7 +549,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { } self.addSubnode(self.scrollNode) - self.scrollNode.addSubnode(self.selectedLineNode) + self.scrollNode.addSubnode(self.selectedBackgroundNode) let reorderingGesture = ReorderingGestureRecognizer(shouldBegin: { [weak self] point in guard let strongSelf = self else { @@ -677,7 +677,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { private var previousSelectedFrame: CGRect? public func cancelAnimations() { - self.selectedLineNode.layer.removeAllAnimations() + self.selectedBackgroundNode.layer.removeAllAnimations() self.scrollNode.layer.removeAllAnimations() } @@ -695,14 +695,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { let previousContentWidth = self.scrollNode.view.contentSize.width if self.currentParams?.presentationData.theme !== presentationData.theme { - self.selectedLineNode.image = generateImage(CGSize(width: 5.0, height: 3.0), rotatedContext: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: 4.0, height: 4.0))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - 4.0, y: 0.0), size: CGSize(width: 4.0, height: 4.0))) - context.fill(CGRect(x: 2.0, y: 0.0, width: size.width - 4.0, height: 4.0)) - context.fill(CGRect(x: 0.0, y: 2.0, width: size.width, height: 2.0)) - })?.resizableImage(withCapInsets: UIEdgeInsets(top: 3.0, left: 3.0, bottom: 0.0, right: 3.0), resizingMode: .stretch) + self.selectedBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: presentationData.theme.chatList.itemHighlightedBackgroundColor) } if isReordering { @@ -864,7 +857,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { let minSpacing: CGFloat = 26.0 - let resolvedSideInset: CGFloat = 16.0 + sideInset + let resolvedSideInset: CGFloat = 25.0 + sideInset var leftOffset: CGFloat = resolvedSideInset var longTitlesWidth: CGFloat = resolvedSideInset @@ -937,13 +930,13 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { } if let selectedFrame = selectedFrame { - let wasAdded = self.selectedLineNode.isHidden - self.selectedLineNode.isHidden = false - let lineFrame = CGRect(origin: CGPoint(x: selectedFrame.minX, y: size.height - 3.0), size: CGSize(width: selectedFrame.width, height: 3.0)) + let wasAdded = self.selectedBackgroundNode.isHidden + self.selectedBackgroundNode.isHidden = false + let selectedBackgroundFrame = CGRect(origin: CGPoint(x: selectedFrame.minX - 10.0, y: selectedFrame.minY - floor((36.0 - selectedFrame.height) * 0.5)), size: CGSize(width: selectedFrame.width + 10.0 * 2.0, height: 36.0)) if wasAdded { - self.selectedLineNode.frame = lineFrame + self.selectedBackgroundNode.frame = selectedBackgroundFrame } else { - transition.updateFrame(node: self.selectedLineNode, frame: lineFrame) + transition.updateFrame(node: self.selectedBackgroundNode, frame: selectedBackgroundFrame) } if let previousSelectedFrame = self.previousSelectedFrame { @@ -972,7 +965,7 @@ public final class ChatListFilterTabContainerNode: ASDisplayNode { self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0) self.previousSelectedFrame = selectedFrame } else { - self.selectedLineNode.isHidden = true + self.selectedBackgroundNode.isHidden = true self.previousSelectedAbsFrame = nil self.previousSelectedFrame = nil } diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD b/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD index 6f94cb0c07..5355cc429f 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD @@ -24,6 +24,8 @@ swift_library( "//submodules/Components/ComponentDisplayAdapters", "//submodules/SearchUI", "//submodules/TelegramUI/Components/MoreHeaderButton", + "//submodules/TelegramUI/Components/EdgeEffect", + "//submodules/TelegramUI/Components/GlassBackgroundComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 8e235da5ea..571c5cfe77 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -9,6 +9,7 @@ import AppBundle import StoryPeerListComponent import TelegramCore import MoreHeaderButton +import GlassBackgroundComponent public final class HeaderNetworkStatusComponent: Component { public enum Content: Equatable { @@ -79,7 +80,6 @@ public final class ChatListHeaderComponent: Component { public let chatListTitle: NetworkStatusTitle? public let leftButton: AnyComponentWithIdentity? public let rightButtons: [AnyComponentWithIdentity] - public let backTitle: String? public let backPressed: (() -> Void)? public init( @@ -89,7 +89,6 @@ public final class ChatListHeaderComponent: Component { chatListTitle: NetworkStatusTitle?, leftButton: AnyComponentWithIdentity?, rightButtons: [AnyComponentWithIdentity], - backTitle: String?, backPressed: (() -> Void)? ) { self.title = title @@ -98,7 +97,6 @@ public final class ChatListHeaderComponent: Component { self.chatListTitle = chatListTitle self.leftButton = leftButton self.rightButtons = rightButtons - self.backTitle = backTitle self.backPressed = backPressed } @@ -121,7 +119,7 @@ public final class ChatListHeaderComponent: Component { if lhs.rightButtons != rhs.rightButtons { return false } - if lhs.backTitle != rhs.backTitle { + if (lhs.backPressed == nil) != (rhs.backPressed == nil) { return false } return true @@ -226,8 +224,6 @@ public final class ChatListHeaderComponent: Component { private let onPressed: () -> Void let arrowView: UIImageView - let titleOffsetContainer: UIView - let titleView: ImmediateTextView private var currentColor: UIColor? @@ -235,16 +231,11 @@ public final class ChatListHeaderComponent: Component { self.onPressed = onPressed self.arrowView = UIImageView() - self.titleOffsetContainer = UIView() - self.titleView = ImmediateTextView() super.init(frame: CGRect()) self.addSubview(self.arrowView) - self.addSubview(self.titleOffsetContainer) - self.titleOffsetContainer.addSubview(self.titleView) - self.highligthedChanged = { [weak self] highlighted in guard let self else { return @@ -266,37 +257,37 @@ public final class ChatListHeaderComponent: Component { self.onPressed() } - func update(title: String, theme: PresentationTheme, availableSize: CGSize, transition: ComponentTransition) -> CGSize { - let titleText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) - let titleTextUpdated = self.titleView.attributedText != titleText - self.titleView.attributedText = titleText - let titleSize = self.titleView.updateLayout(CGSize(width: 100.0, height: 44.0)) - - self.accessibilityLabel = title + func update(theme: PresentationTheme, strings: PresentationStrings, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.accessibilityLabel = strings.Common_Back self.accessibilityTraits = [.button] - if self.currentColor != theme.rootController.navigationBar.accentTextColor { - self.currentColor = theme.rootController.navigationBar.accentTextColor - self.arrowView.image = NavigationBarTheme.generateBackArrowImage(color: theme.rootController.navigationBar.accentTextColor) + if self.currentColor != theme.chat.inputPanel.panelControlColor { + self.currentColor = theme.chat.inputPanel.panelControlColor + let imageSize = CGSize(width: 44.0, height: 44.0) + let topRightPoint = CGPoint(x: 24.6, y: 14.0) + let centerPoint = CGPoint(x: 17.0, y: imageSize.height * 0.5) + self.arrowView.image = generateImage(imageSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor.white.cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.setLineJoin(.round) + context.move(to: topRightPoint) + context.addLine(to: centerPoint) + context.addLine(to: CGPoint(x: topRightPoint.x, y: size.height - topRightPoint.y)) + context.strokePath() + })?.withRenderingMode(.alwaysTemplate) + self.arrowView.tintColor = theme.chat.inputPanel.panelControlColor } - let iconSpacing: CGFloat = 8.0 - let iconOffset: CGFloat = -7.0 - + let size = CGSize(width: 44.0, height: availableSize.height) let arrowSize = self.arrowView.image?.size ?? CGSize(width: 13.0, height: 22.0) - let arrowFrame = CGRect(origin: CGPoint(x: iconOffset - 1.0, y: floor((availableSize.height - arrowSize.height) / 2.0)), size: arrowSize) + let arrowFrame = arrowSize.centered(in: CGRect(origin: CGPoint(), size: size)) transition.setPosition(view: self.arrowView, position: arrowFrame.center) transition.setBounds(view: self.arrowView, bounds: CGRect(origin: CGPoint(), size: arrowFrame.size)) - let titleFrame = CGRect(origin: CGPoint(x: iconOffset - 3.0 + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize) - if titleTextUpdated { - self.titleView.frame = titleFrame - } else { - transition.setFrame(view: self.titleView, frame: titleFrame) - } - - return CGSize(width: iconOffset + arrowSize.width + iconSpacing + titleSize.width, height: availableSize.height) + return size } } @@ -305,9 +296,9 @@ public final class ChatListHeaderComponent: Component { let openStatusSetup: (UIView) -> Void let toggleIsLocked: () -> Void - let leftButtonOffsetContainer: UIView + let leftButtonsContainer: UIView var leftButtonViews: [AnyHashable: ComponentView] = [:] - let rightButtonOffsetContainer: UIView + let rightButtonsContainer: UIView var rightButtonViews: [AnyHashable: ComponentView] = [:] var backButtonView: BackButtonView? @@ -324,6 +315,9 @@ public final class ChatListHeaderComponent: Component { private(set) var centerContentOffsetX: CGFloat = 0.0 private(set) var centerContentOrigin: CGFloat = 0.0 + + private(set) var leftButtonsWidth: CGFloat = 0.0 + private(set) var rightButtonsWidth: CGFloat = 0.0 init( backPressed: @escaping () -> Void, @@ -334,8 +328,9 @@ public final class ChatListHeaderComponent: Component { self.openStatusSetup = openStatusSetup self.toggleIsLocked = toggleIsLocked - self.leftButtonOffsetContainer = UIView() - self.rightButtonOffsetContainer = UIView() + self.leftButtonsContainer = UIView() + self.rightButtonsContainer = UIView() + self.titleOffsetContainer = UIView() self.titleScaleContainer = UIView() @@ -345,8 +340,6 @@ public final class ChatListHeaderComponent: Component { self.addSubview(self.titleOffsetContainer) self.titleOffsetContainer.addSubview(self.titleScaleContainer) - self.addSubview(self.leftButtonOffsetContainer) - self.addSubview(self.rightButtonOffsetContainer) self.titleScaleContainer.addSubview(self.titleTextView) } @@ -393,12 +386,12 @@ public final class ChatListHeaderComponent: Component { transition.setSublayerTransform(view: self.titleOffsetContainer, transform: transform) } - func updateNavigationTransitionAsPrevious(nextView: ContentView, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { - transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: fraction * self.bounds.width * 0.5, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in - completion() - }) - transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) - transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) + func updateNavigationTransitionAsPrevious(nextView: ContentView, width: CGFloat, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) + alphaTransition.setAlpha(view: self.leftButtonsContainer, alpha: pow(1.0 - fraction, 2.0)) + alphaTransition.setBlur(layer: self.leftButtonsContainer.layer, radius: fraction * 10.0) + alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(1.0 - fraction, 2.0)) + alphaTransition.setBlur(layer: self.rightButtonsContainer.layer, radius: fraction * 10.0) if let backButtonView = self.backButtonView { transition.setBounds(view: backButtonView, bounds: CGRect(origin: CGPoint(x: fraction * self.bounds.width * 0.5, y: 0.0), size: backButtonView.bounds.size), completion: { _ in @@ -406,80 +399,53 @@ public final class ChatListHeaderComponent: Component { }) } - if let chatListTitleView = self.chatListTitleView, let nextBackButtonView = nextView.backButtonView { - let titleFrame = chatListTitleView.titleNode.view.convert(chatListTitleView.titleNode.bounds, to: self.titleOffsetContainer) - let backButtonTitleFrame = nextBackButtonView.convert(nextBackButtonView.titleView.frame, to: nextView) - - let totalOffset = titleFrame.minX - backButtonTitleFrame.minX - - transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: totalOffset * fraction, y: 0.0), size: self.titleOffsetContainer.bounds.size)) - transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) - } + let totalOffset = -width * fraction + + transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: totalOffset * fraction, y: 0.0), size: self.titleOffsetContainer.bounds.size)) + transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) } func updateNavigationTransitionAsNext(previousView: ContentView, storyPeerListView: StoryPeerListComponent.View?, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) + transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in completion() }) - transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(fraction, 2.0)) - transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size)) - if let backButtonView = self.backButtonView { - transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0)) - transition.setAlpha(view: backButtonView.arrowView, alpha: pow(fraction, 2.0)) - - if let storyPeerListView { - let previousTitleFrame = storyPeerListView.titleFrame() - let backButtonTitleFrame = backButtonView.convert(backButtonView.titleView.frame, to: self) - - let totalOffset = previousTitleFrame.minX - backButtonTitleFrame.minX - - transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -totalOffset * (1.0 - fraction), y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size)) - transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: pow(fraction, 2.0)) - } else if let previousChatListTitleView = previousView.chatListTitleView { - let previousTitleFrame = previousChatListTitleView.titleNode.view.convert(previousChatListTitleView.titleNode.bounds, to: previousView.titleOffsetContainer) - let backButtonTitleFrame = backButtonView.convert(backButtonView.titleView.frame, to: self) - - let totalOffset = previousTitleFrame.minX - backButtonTitleFrame.minX - - transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -totalOffset * (1.0 - fraction), y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size)) - transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: pow(fraction, 2.0)) - } - } + alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(fraction, 2.0)) + + alphaTransition.setBlur(layer: self.leftButtonsContainer.layer, radius: (1.0 - fraction) * 10.0) + alphaTransition.setBlur(layer: self.rightButtonsContainer.layer, radius: (1.0 - fraction) * 10.0) } func updateNavigationTransitionAsPreviousInplace(nextView: ContentView, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { - transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in - }) - transition.setAlpha(view: self.leftButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) - transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0), completion: { _ in + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) + + alphaTransition.setAlpha(view: self.leftButtonsContainer, alpha: pow(1.0 - fraction, 2.0)) + alphaTransition.setBlur(layer: self.leftButtonsContainer.layer, radius: fraction * 10.0) + alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(1.0 - fraction, 2.0), completion: { _ in completion() }) - - if let backButtonView = self.backButtonView { - transition.setBounds(view: backButtonView, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backButtonView.bounds.size), completion: { _ in - }) - } + alphaTransition.setBlur(layer: self.rightButtonsContainer.layer, radius: fraction * 10.0) transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size)) transition.setAlpha(view: self.titleOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) } func updateNavigationTransitionAsNextInplace(previousView: ContentView, fraction: CGFloat, transition: ComponentTransition, completion: @escaping () -> Void) { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) + transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in completion() }) - transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(fraction, 2.0)) - transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size)) - if let backButtonView = self.backButtonView { - transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0)) - transition.setAlpha(view: backButtonView.arrowView, alpha: pow(fraction, 2.0)) - - transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size)) - transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: pow(fraction, 2.0)) - } + alphaTransition.setBlur(layer: self.leftButtonsContainer.layer, radius: (1.0 - fraction) * 10.0) + alphaTransition.setAlpha(view: self.leftButtonsContainer, alpha: pow(fraction, 2.0)) + alphaTransition.setBlur(layer: self.rightButtonsContainer.layer, radius: (1.0 - fraction) * 10.0) + alphaTransition.setAlpha(view: self.rightButtonsContainer, alpha: pow(fraction, 2.0)) } - func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: ComponentTransition) { + func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, displayBackButton: Bool, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: ComponentTransition) { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.3) + transition.setPosition(view: self.titleOffsetContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: self.titleOffsetContainer.bounds.origin, size: size)) @@ -490,11 +456,10 @@ public final class ChatListHeaderComponent: Component { let titleTextUpdated = self.titleTextView.attributedText != titleText self.titleTextView.attributedText = titleText - let buttonSpacing: CGFloat = 8.0 + let buttonSpacing: CGFloat = 0.0 + var nextLeftButtonX: CGFloat = 0.0 - var leftOffset = sideInset - - if let backTitle = backTitle { + if displayBackButton { var backButtonTransition = transition let backButtonView: BackButtonView if let current = self.backButtonView { @@ -508,11 +473,14 @@ public final class ChatListHeaderComponent: Component { self.backPressed() }) self.backButtonView = backButtonView - self.addSubview(backButtonView) + self.leftButtonsContainer.addSubview(backButtonView) } - let backButtonSize = backButtonView.update(title: backTitle, theme: theme, availableSize: CGSize(width: 100.0, height: size.height), transition: backButtonTransition) - backButtonTransition.setFrame(view: backButtonView, frame: CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - backButtonSize.height) / 2.0)), size: backButtonSize)) - leftOffset += backButtonSize.width + buttonSpacing + let backButtonSize = backButtonView.update(theme: theme, strings: strings, availableSize: CGSize(width: 100.0, height: size.height), transition: backButtonTransition) + backButtonTransition.setFrame(view: backButtonView, frame: CGRect(origin: CGPoint(x: nextLeftButtonX, y: floor((size.height - backButtonSize.height) / 2.0)), size: backButtonSize)) + if nextLeftButtonX != 0.0 { + nextLeftButtonX += buttonSpacing + } + nextLeftButtonX += backButtonSize.width } else if let backButtonView = self.backButtonView { self.backButtonView = nil backButtonView.removeFromSuperview() @@ -521,6 +489,10 @@ public final class ChatListHeaderComponent: Component { var validLeftButtons = Set() if let leftButton = content.leftButton { validLeftButtons.insert(leftButton.id) + + if nextLeftButtonX != 0.0 { + nextLeftButtonX += buttonSpacing + } var buttonTransition = transition var animateButtonIn = false @@ -541,23 +513,25 @@ public final class ChatListHeaderComponent: Component { }, containerSize: CGSize(width: 100.0, height: size.height) ) - let buttonFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: nextLeftButtonX, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) if let buttonComponentView = buttonView.view { if buttonComponentView.superview == nil { - self.leftButtonOffsetContainer.addSubview(buttonComponentView) + self.leftButtonsContainer.addSubview(buttonComponentView) } buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame) if animateButtonIn { - transition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) + alphaTransition.animateBlur(layer: buttonComponentView.layer, fromRadius: 10.0, toRadius: 0.0) + alphaTransition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) } } - leftOffset = buttonFrame.maxX + buttonSpacing + nextLeftButtonX += buttonSize.width } var removeLeftButtons: [AnyHashable] = [] for (id, buttonView) in self.leftButtonViews { if !validLeftButtons.contains(id) { if let buttonComponentView = buttonView.view { - transition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in + alphaTransition.setBlur(layer: buttonComponentView.layer, radius: 10.0) + alphaTransition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in buttonComponentView?.removeFromSuperview() }) } @@ -568,10 +542,14 @@ public final class ChatListHeaderComponent: Component { self.leftButtonViews.removeValue(forKey: id) } - var rightOffset = size.width - sideInset + var nextRightButtonX: CGFloat = 0.0 var validRightButtons = Set() - for rightButton in content.rightButtons { + for rightButton in content.rightButtons.reversed() { validRightButtons.insert(rightButton.id) + + if nextRightButtonX != 0.0 { + nextRightButtonX += buttonSpacing + } var buttonTransition = transition var animateButtonIn = false @@ -592,23 +570,25 @@ public final class ChatListHeaderComponent: Component { }, containerSize: CGSize(width: 100.0, height: size.height) ) - let buttonFrame = CGRect(origin: CGPoint(x: rightOffset - buttonSize.width, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) + let buttonFrame = CGRect(origin: CGPoint(x: nextRightButtonX, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) if let buttonComponentView = buttonView.view { if buttonComponentView.superview == nil { - self.rightButtonOffsetContainer.addSubview(buttonComponentView) + self.rightButtonsContainer.addSubview(buttonComponentView) } buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame) if animateButtonIn { - transition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) + alphaTransition.animateBlur(layer: buttonComponentView.layer, fromRadius: 10.0, toRadius: 0.0) + alphaTransition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) } } - rightOffset = buttonFrame.minX - buttonSpacing + nextRightButtonX += buttonSize.width } var removeRightButtons: [AnyHashable] = [] for (id, buttonView) in self.rightButtonViews { if !validRightButtons.contains(id) { if let buttonComponentView = buttonView.view { - transition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in + alphaTransition.setBlur(layer: buttonComponentView.layer, radius: 10.0) + alphaTransition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in buttonComponentView?.removeFromSuperview() }) } @@ -618,8 +598,11 @@ public final class ChatListHeaderComponent: Component { for id in removeRightButtons { self.rightButtonViews.removeValue(forKey: id) } - - let commonInset: CGFloat = max(leftOffset, size.width - rightOffset) + + self.leftButtonsWidth = nextLeftButtonX + self.rightButtonsWidth = nextRightButtonX + + let commonInset: CGFloat = sideInset + max(nextLeftButtonX, nextRightButtonX) + 8.0 let remainingWidth = size.width - commonInset * 2.0 let titleTextSize = self.titleTextView.updateLayout(CGSize(width: remainingWidth, height: size.height)) @@ -662,10 +645,10 @@ public final class ChatListHeaderComponent: Component { } var centerContentLeftInset: CGFloat = 0.0 - centerContentLeftInset = leftOffset - 4.0 + centerContentLeftInset = nextLeftButtonX + 4.0 var centerContentRightInset: CGFloat = 0.0 - centerContentRightInset = size.width - rightOffset - 8.0 + centerContentRightInset = nextRightButtonX + 20.0 var centerContentWidth: CGFloat = 0.0 var centerContentOffsetX: CGFloat = 0.0 @@ -690,10 +673,8 @@ public final class ChatListHeaderComponent: Component { let titleContentRect = chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), transition: transition.containedViewLayoutTransition) centerContentWidth = floor((chatListTitleContentSize.width * 0.5 - titleContentRect.minX) * 2.0) - //sideWidth + centerWidth + centerOffset = size.width - //let centerOffset = -(size.width - (sideContentWidth + centerContentWidth)) * 0.5 + size.width * 0.5 let centerOffset = sideContentWidth * 0.5 - centerContentOffsetX = -max(0.0, centerOffset + titleContentRect.maxX - 2.0 - rightOffset) + centerContentOffsetX = -max(0.0, centerOffset + titleContentRect.maxX - 2.0 - (size.width - sideInset - nextRightButtonX)) chatListTitleView.openStatusSetup = { [weak self] sourceView in guard let self else { @@ -741,6 +722,11 @@ public final class ChatListHeaderComponent: Component { private var primaryContentView: ContentView? private var secondaryContentView: ContentView? private var storyOffsetFraction: CGFloat = 0.0 + + private let leftButtonsContainer: UIView + private let rightButtonsContainer: UIView + private var leftButtonsBackgroundContainer: GlassBackgroundView? + private var rightButtonsBackgroundContainer: GlassBackgroundView? private let storyPeerListExternalState = StoryPeerListComponent.ExternalState() private var storyPeerList: ComponentView? @@ -753,6 +739,10 @@ public final class ChatListHeaderComponent: Component { } override init(frame: CGRect) { + self.leftButtonsContainer = UIView() + self.rightButtonsContainer = UIView() + self.rightButtonsContainer.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0) + super.init(frame: frame) self.storyOffsetFraction = 1.0 @@ -766,10 +756,6 @@ public final class ChatListHeaderComponent: Component { return self.effectiveContentView?.backButtonView?.arrowView } - public var backButtonTitleView: UIView? { - return self.effectiveContentView?.backButtonView?.titleView - } - public var rightButtonView: UIView? { return self.effectiveContentView?.rightButtonViews.first?.value.view } @@ -795,18 +781,6 @@ public final class ChatListHeaderComponent: Component { } } - public func makeTransitionBackButtonView(accentColor: UIColor) -> UIView? { - if let backButtonTitleView = self.backButtonTitleView as? ImmediateTextView { - let view = ImmediateTextView() - view.attributedText = NSAttributedString(string: backButtonTitleView.attributedText?.string ?? "", font: Font.regular(17.0), textColor: accentColor) - let _ = view.updateLayout(CGSize(width: 100.0, height: 100.0)) - view.frame = backButtonTitleView.convert(backButtonTitleView.bounds, to: self) - return view - } else { - return nil - } - } - public func storyPeerListView() -> StoryPeerListComponent.View? { return self.storyPeerList?.view as? StoryPeerListComponent.View } @@ -848,8 +822,8 @@ public final class ChatListHeaderComponent: Component { let previousComponent = self.component self.component = component + var primaryContentTransition = transition if var primaryContent = component.primaryContent { - var primaryContentTransition = transition let primaryContentView: ContentView if let current = self.primaryContentView { primaryContentView = current @@ -877,6 +851,8 @@ public final class ChatListHeaderComponent: Component { ) self.primaryContentView = primaryContentView self.addSubview(primaryContentView) + self.leftButtonsContainer.addSubview(primaryContentView.leftButtonsContainer) + self.rightButtonsContainer.addSubview(primaryContentView.rightButtonsContainer) } let sideContentWidth: CGFloat = 0.0 @@ -889,18 +865,19 @@ public final class ChatListHeaderComponent: Component { chatListTitle: nil, leftButton: primaryContent.leftButton, rightButtons: primaryContent.rightButtons, - backTitle: primaryContent.backTitle, backPressed: primaryContent.backPressed ) } - primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition) + primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, displayBackButton: primaryContent.backPressed != nil, sideInset: component.sideInset, sideContentWidth: sideContentWidth, sideContentFraction: (1.0 - component.storiesFraction), size: availableSize, transition: primaryContentTransition) primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) primaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: primaryContentTransition) } else if let primaryContentView = self.primaryContentView { self.primaryContentView = nil primaryContentView.removeFromSuperview() + primaryContentView.leftButtonsContainer.removeFromSuperview() + primaryContentView.rightButtonsContainer.removeFromSuperview() } var storyListTransition = transition @@ -991,13 +968,16 @@ public final class ChatListHeaderComponent: Component { ) } + var secondaryContentTransition = transition + var secondaryContentIsAnimatingIn = false + var removedSecondaryContentView: ContentView? if let secondaryContent = component.secondaryContent { - var secondaryContentTransition = transition let secondaryContentView: ContentView if let current = self.secondaryContentView { secondaryContentView = current } else { secondaryContentTransition = .immediate + secondaryContentIsAnimatingIn = true secondaryContentView = ContentView( backPressed: { [weak self] in guard let self, let component = self.component else { @@ -1019,9 +999,15 @@ public final class ChatListHeaderComponent: Component { } ) self.secondaryContentView = secondaryContentView - self.addSubview(secondaryContentView) + if let primaryContentView = self.primaryContentView { + self.insertSubview(secondaryContentView, aboveSubview: primaryContentView) + } else { + self.addSubview(secondaryContentView) + } + self.leftButtonsContainer.addSubview(secondaryContentView.leftButtonsContainer) + self.rightButtonsContainer.addSubview(secondaryContentView.rightButtonsContainer) } - secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.navigationBackTitle ?? component.primaryContent?.title, sideInset: component.sideInset, sideContentWidth: 0.0, sideContentFraction: 0.0, size: availableSize, transition: secondaryContentTransition) + secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, displayBackButton: true, sideInset: component.sideInset, sideContentWidth: 0.0, sideContentFraction: 0.0, size: availableSize, transition: secondaryContentTransition) secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition) @@ -1032,7 +1018,7 @@ public final class ChatListHeaderComponent: Component { primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {}) secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {}) } else { - primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {}) + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, width: availableSize.width, fraction: 0.0, transition: .immediate, completion: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, storyPeerListView: self.storyPeerListView(), fraction: 0.0, transition: .immediate, completion: {}) } } @@ -1041,26 +1027,33 @@ public final class ChatListHeaderComponent: Component { primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) } else { - primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, width: availableSize.width, fraction: component.secondaryTransition, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, storyPeerListView: self.storyPeerListView(), fraction: component.secondaryTransition, transition: transition, completion: {}) } } } else if let secondaryContentView = self.secondaryContentView { self.secondaryContentView = nil + removedSecondaryContentView = secondaryContentView if let primaryContentView = self.primaryContentView { if self.storyOffsetFraction < 0.8 { primaryContentView.updateNavigationTransitionAsPreviousInplace(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNextInplace(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in + secondaryContentView?.leftButtonsContainer.removeFromSuperview() + secondaryContentView?.rightButtonsContainer.removeFromSuperview() secondaryContentView?.removeFromSuperview() }) } else { - primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {}) + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, width: availableSize.width, fraction: 0.0, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, storyPeerListView: self.storyPeerListView(), fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in + secondaryContentView?.leftButtonsContainer.removeFromSuperview() + secondaryContentView?.rightButtonsContainer.removeFromSuperview() secondaryContentView?.removeFromSuperview() }) } } else { + secondaryContentView.leftButtonsContainer.removeFromSuperview() + secondaryContentView.rightButtonsContainer.removeFromSuperview() secondaryContentView.removeFromSuperview() } } @@ -1070,18 +1063,10 @@ public final class ChatListHeaderComponent: Component { self.addSubview(storyPeerListComponentView) } - //let storyPeerListMinOffset: CGFloat = -7.0 let storyPeerListMaxOffset: CGFloat = availableSize.height + 2.0 - //let storyPeerListPosition: CGFloat = storyPeerListMinOffset * (1.0 - component.storiesFraction) + storyPeerListMaxOffset * component.storiesFraction - var storiesX: CGFloat = 0.0 - if let nextBackButtonView = self.secondaryContentView?.backButtonView { - let backButtonTitleFrame = nextBackButtonView.convert(nextBackButtonView.titleView.frame, to: self) - let storyListTitleFrame = storyPeerListComponentView.titleFrame() - - storiesX += (backButtonTitleFrame.minX - storyListTitleFrame.minX) * component.secondaryTransition - } + storiesX -= availableSize.width * component.secondaryTransition storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: storiesX, y: storyPeerListMaxOffset), size: CGSize(width: availableSize.width, height: 79.0))) @@ -1090,6 +1075,84 @@ public final class ChatListHeaderComponent: Component { let storyListAlpha: CGFloat = (1.0 - component.secondaryTransition) * storyListNormalAlpha storyListTransition.setAlpha(view: storyPeerListComponentView, alpha: storyListAlpha) } + + var leftButtonsEffectiveWidth: CGFloat = 0.0 + var rightButtonsEffectiveWidth: CGFloat = 0.0 + if let primaryContentView = self.primaryContentView, let secondaryContentView = self.secondaryContentView { + + leftButtonsEffectiveWidth = primaryContentView.leftButtonsWidth * (1.0 - component.secondaryTransition) + secondaryContentView.leftButtonsWidth * component.secondaryTransition + rightButtonsEffectiveWidth = primaryContentView.rightButtonsWidth * (1.0 - component.secondaryTransition) + secondaryContentView.rightButtonsWidth * component.secondaryTransition + + primaryContentTransition.setFrame(view: primaryContentView.leftButtonsContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: max(44.0, primaryContentView.leftButtonsWidth), height: 44.0))) + secondaryContentTransition.setFrame(view: secondaryContentView.leftButtonsContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: max(44.0, secondaryContentView.leftButtonsWidth), height: 44.0))) + + primaryContentTransition.setFrame(view: primaryContentView.rightButtonsContainer, frame: CGRect(origin: CGPoint(x: rightButtonsEffectiveWidth - primaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, primaryContentView.rightButtonsWidth), height: 44.0))) + + if secondaryContentIsAnimatingIn { + secondaryContentView.rightButtonsContainer.frame = CGRect(origin: CGPoint(x: self.rightButtonsContainer.bounds.width - secondaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, secondaryContentView.rightButtonsWidth), height: 44.0)) + } + transition.setFrame(view: secondaryContentView.rightButtonsContainer, frame: CGRect(origin: CGPoint(x: rightButtonsEffectiveWidth - secondaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, secondaryContentView.rightButtonsWidth), height: 44.0))) + } else if let primaryContentView = self.primaryContentView { + leftButtonsEffectiveWidth = primaryContentView.leftButtonsWidth + rightButtonsEffectiveWidth = primaryContentView.rightButtonsWidth + + primaryContentTransition.setFrame(view: primaryContentView.leftButtonsContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: max(44.0, primaryContentView.leftButtonsWidth), height: 44.0))) + primaryContentTransition.setFrame(view: primaryContentView.rightButtonsContainer, frame: CGRect(origin: CGPoint(x: rightButtonsEffectiveWidth - primaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, primaryContentView.rightButtonsWidth), height: 44.0))) + + if let removedSecondaryContentView { + transition.setFrame(view: removedSecondaryContentView.rightButtonsContainer, frame: CGRect(origin: CGPoint(x: rightButtonsEffectiveWidth - removedSecondaryContentView.rightButtonsWidth, y: 0.0), size: CGSize(width: max(44.0, removedSecondaryContentView.rightButtonsWidth), height: 44.0))) + } + } + + if leftButtonsEffectiveWidth != 0.0 { + let leftButtonsBackgroundContainer: GlassBackgroundView + var leftButtonsBackgroundContainerTransition = transition + if let current = self.leftButtonsBackgroundContainer { + leftButtonsBackgroundContainer = current + } else { + leftButtonsBackgroundContainerTransition = leftButtonsBackgroundContainerTransition.withAnimation(.none) + leftButtonsBackgroundContainer = GlassBackgroundView() + self.leftButtonsBackgroundContainer = leftButtonsBackgroundContainer + self.addSubview(leftButtonsBackgroundContainer) + leftButtonsBackgroundContainer.contentView.addSubview(self.leftButtonsContainer) + } + let leftButtonsContainerFrame = CGRect(origin: CGPoint(x: component.sideInset, y: 0.0), size: CGSize(width: max(44.0, leftButtonsEffectiveWidth), height: 44.0)) + leftButtonsBackgroundContainerTransition.setFrame(view: leftButtonsBackgroundContainer, frame: leftButtonsContainerFrame) + leftButtonsBackgroundContainer.update(size: leftButtonsContainerFrame.size, cornerRadius: leftButtonsContainerFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: leftButtonsBackgroundContainerTransition) + leftButtonsBackgroundContainerTransition.setFrame(view: self.leftButtonsContainer, frame: CGRect(origin: CGPoint(), size: leftButtonsContainerFrame.size)) + } else { + if let leftButtonsBackgroundContainer = self.leftButtonsBackgroundContainer { + self.leftButtonsBackgroundContainer = nil + transition.setAlpha(view: leftButtonsBackgroundContainer, alpha: 0.0, completion: { [weak leftButtonsBackgroundContainer] _ in + leftButtonsBackgroundContainer?.removeFromSuperview() + }) + } + } + + if rightButtonsEffectiveWidth != 0.0 { + let rightButtonsBackgroundContainer: GlassBackgroundView + var rightButtonsBackgroundContainerTransition = transition + if let current = self.rightButtonsBackgroundContainer { + rightButtonsBackgroundContainer = current + } else { + rightButtonsBackgroundContainerTransition = rightButtonsBackgroundContainerTransition.withAnimation(.none) + rightButtonsBackgroundContainer = GlassBackgroundView() + self.rightButtonsBackgroundContainer = rightButtonsBackgroundContainer + self.addSubview(rightButtonsBackgroundContainer) + rightButtonsBackgroundContainer.contentView.addSubview(self.rightButtonsContainer) + } + let rightButtonsContainerFrame = CGRect(origin: CGPoint(x: availableSize.width - component.sideInset - max(44.0, rightButtonsEffectiveWidth), y: 0.0), size: CGSize(width: max(44.0, rightButtonsEffectiveWidth), height: 44.0)) + rightButtonsBackgroundContainerTransition.setFrame(view: rightButtonsBackgroundContainer, frame: rightButtonsContainerFrame) + rightButtonsBackgroundContainer.update(size: rightButtonsContainerFrame.size, cornerRadius: rightButtonsContainerFrame.height * 0.5, isDark: component.theme.overallDarkAppearance, tintColor: .init(kind: .panel, color: UIColor(white: component.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: rightButtonsBackgroundContainerTransition) + rightButtonsBackgroundContainerTransition.setFrame(view: self.rightButtonsContainer, frame: CGRect(origin: CGPoint(), size: rightButtonsContainerFrame.size)) + } else { + if let rightButtonsBackgroundContainer = self.rightButtonsBackgroundContainer { + self.rightButtonsBackgroundContainer = nil + transition.setAlpha(view: rightButtonsBackgroundContainer, alpha: 0.0, completion: { [weak rightButtonsBackgroundContainer] _ in + rightButtonsBackgroundContainer?.removeFromSuperview() + }) + } + } return availableSize } @@ -1119,251 +1182,3 @@ public final class ChatListHeaderComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } - -public final class NavigationButtonComponentEnvironment: Equatable { - public let theme: PresentationTheme - - public init(theme: PresentationTheme) { - self.theme = theme - } - - public static func ==(lhs: NavigationButtonComponentEnvironment, rhs: NavigationButtonComponentEnvironment) -> Bool { - if lhs.theme != rhs.theme { - return false - } - return true - } -} - -public final class NavigationButtonComponent: Component { - public typealias EnvironmentType = NavigationButtonComponentEnvironment - - public enum Content: Equatable { - case text(title: String, isBold: Bool) - case more - case icon(imageName: String) - case proxy(status: ChatTitleProxyStatus) - } - - public let content: Content - public let pressed: (UIView) -> Void - public let contextAction: ((UIView, ContextGesture?) -> Void)? - - public init( - content: Content, - pressed: @escaping (UIView) -> Void, - contextAction: ((UIView, ContextGesture?) -> Void)? = nil - ) { - self.content = content - self.pressed = pressed - self.contextAction = contextAction - } - - public static func ==(lhs: NavigationButtonComponent, rhs: NavigationButtonComponent) -> Bool { - if lhs.content != rhs.content { - return false - } - return true - } - - public final class View: HighlightTrackingButton { - private var textView: ImmediateTextView? - - private var iconView: UIImageView? - private var iconImageName: String? - - private var proxyNode: ChatTitleProxyNode? - - private var moreButton: MoreHeaderButton? - - private var component: NavigationButtonComponent? - private var theme: PresentationTheme? - - override init(frame: CGRect) { - super.init(frame: frame) - - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) - - self.highligthedChanged = { [weak self] highlighted in - guard let self else { - return - } - if highlighted { - self.textView?.alpha = 0.6 - self.proxyNode?.alpha = 0.6 - self.iconView?.alpha = 0.6 - } else { - self.textView?.alpha = 1.0 - self.textView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - - self.proxyNode?.alpha = 1.0 - self.proxyNode?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - - self.iconView?.alpha = 1.0 - self.iconView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) - } - } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc private func pressed() { - self.component?.pressed(self) - } - - func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - self.component = component - - let theme = environment[NavigationButtonComponentEnvironment.self].value.theme - var themeUpdated = false - if self.theme !== theme { - self.theme = theme - themeUpdated = true - } - - let iconOffset: CGFloat = 4.0 - - var textString: NSAttributedString? - var imageName: String? - var proxyStatus: ChatTitleProxyStatus? - var isMore: Bool = false - - switch component.content { - case let .text(title, isBold): - textString = NSAttributedString(string: title, font: isBold ? Font.bold(17.0) : Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) - case .more: - isMore = true - case let .icon(imageNameValue): - imageName = imageNameValue - case let .proxy(status): - proxyStatus = status - } - - var size = CGSize(width: 0.0, height: availableSize.height) - - if let textString = textString { - let textView: ImmediateTextView - if let current = self.textView { - textView = current - } else { - textView = ImmediateTextView() - textView.isUserInteractionEnabled = false - self.textView = textView - self.addSubview(textView) - } - - textView.attributedText = textString - let textSize = textView.updateLayout(availableSize) - size.width = textSize.width - - textView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((availableSize.height - textSize.height) / 2.0)), size: textSize) - } else if let textView = self.textView { - self.textView = nil - textView.removeFromSuperview() - } - - if let imageName = imageName { - let iconView: UIImageView - if let current = self.iconView { - iconView = current - } else { - iconView = UIImageView() - iconView.isUserInteractionEnabled = false - self.iconView = iconView - self.addSubview(iconView) - } - if self.iconImageName != imageName || themeUpdated { - self.iconImageName = imageName - iconView.image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.rootController.navigationBar.accentTextColor) - } - - if let iconSize = iconView.image?.size { - size.width = iconSize.width - - iconView.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize) - } - } else if let iconView = self.iconView { - self.iconView = nil - iconView.removeFromSuperview() - self.iconImageName = nil - } - - if let proxyStatus = proxyStatus { - let proxyNode: ChatTitleProxyNode - if let current = self.proxyNode { - proxyNode = current - } else { - proxyNode = ChatTitleProxyNode(theme: theme) - proxyNode.isUserInteractionEnabled = false - self.proxyNode = proxyNode - self.addSubnode(proxyNode) - } - - let proxySize = CGSize(width: 30.0, height: 30.0) - size.width = proxySize.width - - proxyNode.theme = theme - proxyNode.status = proxyStatus - - proxyNode.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - proxySize.height) / 2.0)), size: proxySize) - } else if let proxyNode = self.proxyNode { - self.proxyNode = nil - proxyNode.removeFromSupernode() - } - - if isMore { - let moreButton: MoreHeaderButton - if let current = self.moreButton, !themeUpdated { - moreButton = current - } else { - if let moreButton = self.moreButton { - moreButton.removeFromSupernode() - self.moreButton = nil - } - - moreButton = MoreHeaderButton(color: theme.rootController.navigationBar.buttonColor) - moreButton.isUserInteractionEnabled = true - moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) - moreButton.onPressed = { [weak self] in - guard let self, let component = self.component else { - return - } - self.moreButton?.play() - component.pressed(self) - } - moreButton.contextAction = { [weak self] sourceNode, gesture in - guard let self, let component = self.component else { - return - } - self.moreButton?.play() - component.contextAction?(self, gesture) - } - self.moreButton = moreButton - self.addSubnode(moreButton) - } - - let buttonSize = CGSize(width: 26.0, height: 44.0) - size.width = buttonSize.width - - moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) - - moreButton.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - buttonSize.height) / 2.0)), size: buttonSize) - } else if let moreButton = self.moreButton { - self.moreButton = nil - moreButton.removeFromSupernode() - } - - return size - } - } - - public func makeView() -> View { - return View(frame: CGRect()) - } - - public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift index 549c6fb4fd..dc26cb74e3 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListNavigationBar.swift @@ -9,6 +9,7 @@ import SearchUI import AccountContext import TelegramCore import StoryPeerListComponent +import EdgeEffect public final class ChatListNavigationBar: Component { public final class AnimationHint { @@ -20,14 +21,22 @@ public final class ChatListNavigationBar: Component { self.crossfadeStoryPeers = crossfadeStoryPeers } } + + public struct Search: Equatable { + public var isEnabled: Bool + + public init(isEnabled: Bool) { + self.isEnabled = isEnabled + } + } public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings public let statusBarHeight: CGFloat public let sideInset: CGFloat + public let search: Search? public let isSearchActive: Bool - public let isSearchEnabled: Bool public let primaryContent: ChatListHeaderComponent.Content? public let secondaryContent: ChatListHeaderComponent.Content? public let secondaryTransition: CGFloat @@ -48,8 +57,8 @@ public final class ChatListNavigationBar: Component { strings: PresentationStrings, statusBarHeight: CGFloat, sideInset: CGFloat, + search: Search?, isSearchActive: Bool, - isSearchEnabled: Bool, primaryContent: ChatListHeaderComponent.Content?, secondaryContent: ChatListHeaderComponent.Content?, secondaryTransition: CGFloat, @@ -69,8 +78,8 @@ public final class ChatListNavigationBar: Component { self.strings = strings self.statusBarHeight = statusBarHeight self.sideInset = sideInset + self.search = search self.isSearchActive = isSearchActive - self.isSearchEnabled = isSearchEnabled self.primaryContent = primaryContent self.secondaryContent = secondaryContent self.secondaryTransition = secondaryTransition @@ -102,10 +111,10 @@ public final class ChatListNavigationBar: Component { if lhs.sideInset != rhs.sideInset { return false } - if lhs.isSearchActive != rhs.isSearchActive { + if lhs.search != rhs.search { return false } - if lhs.isSearchEnabled != rhs.isSearchEnabled { + if lhs.isSearchActive != rhs.isSearchActive { return false } if lhs.primaryContent != rhs.primaryContent { @@ -155,8 +164,7 @@ public final class ChatListNavigationBar: Component { }() public final class View: UIView { - private let backgroundView: BlurredBackgroundView - private let separatorLayer: SimpleLayer + private let edgeEffectView: EdgeEffectView public let headerContent = ComponentView() @@ -184,17 +192,15 @@ public final class ChatListNavigationBar: Component { private var disappearingTabsViewSearch: Bool = false private var currentHeaderComponent: ChatListHeaderComponent? + + private var currentHeight: CGFloat = 0.0 override public init(frame: CGRect) { - self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) - self.backgroundView.layer.anchorPoint = CGPoint(x: 0.0, y: 1.0) - self.separatorLayer = SimpleLayer() - self.separatorLayer.anchorPoint = CGPoint() + self.edgeEffectView = EdgeEffectView() super.init(frame: frame) - self.addSubview(self.backgroundView) - self.layer.addSublayer(self.separatorLayer) + self.addSubview(self.edgeEffectView) } required init?(coder: NSCoder) { @@ -202,7 +208,7 @@ public final class ChatListNavigationBar: Component { } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if !self.backgroundView.frame.contains(point) { + if point.y >= self.currentHeight { return nil } @@ -248,7 +254,12 @@ public final class ChatListNavigationBar: Component { let searchOffsetDistance: CGFloat = ChatListNavigationBar.searchScrollHeight - let minContentOffset: CGFloat = ChatListNavigationBar.searchScrollHeight + let minContentOffset: CGFloat + if component.search != nil { + minContentOffset = ChatListNavigationBar.searchScrollHeight + } else { + minContentOffset = 0.0 + } let clippedScrollOffset = min(minContentOffset, offset) if self.clippedScrollOffset == clippedScrollOffset && !self.hasDeferredScrollOffset && !forceUpdate && !allowAvatarsExpansionUpdated { @@ -259,72 +270,87 @@ public final class ChatListNavigationBar: Component { let visibleSize = CGSize(width: currentLayout.size.width, height: max(0.0, currentLayout.size.height - clippedScrollOffset)) - let previousHeight = self.separatorLayer.position.y + let previousHeight = self.currentHeight - self.backgroundView.update(size: CGSize(width: visibleSize.width, height: 1000.0), transition: transition.containedViewLayoutTransition) + /*self.backgroundView.update(size: CGSize(width: visibleSize.width, height: 1000.0), transition: transition.containedViewLayoutTransition) transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: CGPoint(), size: CGSize(width: visibleSize.width, height: 1000.0))) transition.animatePosition(view: self.backgroundView, from: CGPoint(x: 0.0, y: -visibleSize.height + self.backgroundView.layer.position.y), to: CGPoint(), additive: true) - self.backgroundView.layer.position = CGPoint(x: 0.0, y: visibleSize.height) + self.backgroundView.layer.position = CGPoint(x: 0.0, y: visibleSize.height)*/ + + let edgeEffectHeight: CGFloat = currentLayout.size.height + 20.0 + var edgeEffectFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: currentLayout.size.width, height: edgeEffectHeight)) + if component.isSearchActive { + edgeEffectFrame.origin.y -= 46.0 + } + transition.setFrame(view: self.edgeEffectView, frame: edgeEffectFrame) + self.edgeEffectView.update(content: component.theme.list.plainBackgroundColor, blur: true, alpha: 0.75, rect: edgeEffectFrame, edge: .top, edgeSize: 40.0, transition: transition) - transition.setFrameWithAdditivePosition(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: UIScreenPixel))) + self.currentHeight = visibleSize.height - let searchContentNode: NavigationBarSearchContentNode - if let current = self.searchContentNode { - searchContentNode = current - - if themeUpdated { + if let search = component.search { + let searchContentNode: NavigationBarSearchContentNode + if let current = self.searchContentNode { + searchContentNode = current + + if themeUpdated { + let placeholder: String + let compactPlaceholder: String + + placeholder = component.strings.Common_Search + compactPlaceholder = component.strings.Common_Search + + searchContentNode.updateThemeAndPlaceholder(theme: component.theme, placeholder: placeholder, compactPlaceholder: compactPlaceholder) + } + } else { let placeholder: String let compactPlaceholder: String placeholder = component.strings.Common_Search compactPlaceholder = component.strings.Common_Search - searchContentNode.updateThemeAndPlaceholder(theme: component.theme, placeholder: placeholder, compactPlaceholder: compactPlaceholder) - } - } else { - let placeholder: String - let compactPlaceholder: String - - placeholder = component.strings.Common_Search - compactPlaceholder = component.strings.Common_Search - - searchContentNode = NavigationBarSearchContentNode( - theme: component.theme, - placeholder: placeholder, - compactPlaceholder: compactPlaceholder, - activate: { [weak self] in - guard let self, let component = self.component, let searchContentNode = self.searchContentNode else { - return + searchContentNode = NavigationBarSearchContentNode( + theme: component.theme, + placeholder: placeholder, + compactPlaceholder: compactPlaceholder, + activate: { [weak self] in + guard let self, let component = self.component, let searchContentNode = self.searchContentNode else { + return + } + component.activateSearch(searchContentNode) } - component.activateSearch(searchContentNode) - } - ) - searchContentNode.view.layer.anchorPoint = CGPoint() - self.searchContentNode = searchContentNode - self.addSubview(searchContentNode.view) + ) + searchContentNode.view.layer.anchorPoint = CGPoint() + self.searchContentNode = searchContentNode + self.addSubview(searchContentNode.view) + } + + let searchSize = CGSize(width: currentLayout.size.width - 6.0 * 2.0, height: navigationBarSearchContentHeight) + var searchFrame = CGRect(origin: CGPoint(x: 6.0, y: visibleSize.height - searchSize.height), size: searchSize) + if component.tabsNode != nil { + searchFrame.origin.y -= 40.0 + } + if !component.isSearchActive { + searchFrame.origin.y -= component.accessoryPanelContainerHeight + } + + let clippedSearchOffset = max(0.0, min(clippedScrollOffset, searchOffsetDistance)) + let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance + searchContentNode.expansionProgress = 1.0 - searchOffsetFraction + + transition.setFrameWithAdditivePosition(view: searchContentNode.view, frame: searchFrame) + + searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition) + + transition.setAlpha(view: searchContentNode.view, alpha: search.isEnabled ? 1.0 : 0.5) + searchContentNode.isUserInteractionEnabled = search.isEnabled + } else { + if let searchContentNode = self.searchContentNode { + self.searchContentNode = nil + searchContentNode.view.removeFromSuperview() + } } - let searchSize = CGSize(width: currentLayout.size.width - 6.0 * 2.0, height: navigationBarSearchContentHeight) - var searchFrame = CGRect(origin: CGPoint(x: 6.0, y: visibleSize.height - searchSize.height), size: searchSize) - if component.tabsNode != nil { - searchFrame.origin.y -= 40.0 - } - if !component.isSearchActive { - searchFrame.origin.y -= component.accessoryPanelContainerHeight - } - - let clippedSearchOffset = max(0.0, min(clippedScrollOffset, searchOffsetDistance)) - let searchOffsetFraction = clippedSearchOffset / searchOffsetDistance - searchContentNode.expansionProgress = 1.0 - searchOffsetFraction - - transition.setFrameWithAdditivePosition(view: searchContentNode.view, frame: searchFrame) - - searchContentNode.updateLayout(size: searchSize, leftInset: component.sideInset, rightInset: component.sideInset, transition: transition.containedViewLayoutTransition) - - transition.setAlpha(view: searchContentNode.view, alpha: component.isSearchEnabled ? 1.0 : 0.5) - searchContentNode.isUserInteractionEnabled = component.isSearchEnabled - let headerTransition = transition let storiesOffsetFraction: CGFloat @@ -455,7 +481,8 @@ public final class ChatListNavigationBar: Component { } var tabsFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: 46.0)) - if !component.isSearchActive { + if component.isSearchActive { + } else { tabsFrame.origin.y -= component.accessoryPanelContainerHeight } if component.tabsNode != nil { @@ -463,7 +490,8 @@ public final class ChatListNavigationBar: Component { } var accessoryPanelContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: visibleSize.height), size: CGSize(width: visibleSize.width, height: component.accessoryPanelContainerHeight)) - if !component.isSearchActive { + if component.isSearchActive { + } else { accessoryPanelContainerFrame.origin.y -= component.accessoryPanelContainerHeight } @@ -528,8 +556,8 @@ public final class ChatListNavigationBar: Component { strings: component.strings, statusBarHeight: component.statusBarHeight, sideInset: component.sideInset, + search: component.search, isSearchActive: component.isSearchActive, - isSearchEnabled: component.isSearchEnabled, primaryContent: component.primaryContent, secondaryContent: component.secondaryContent, secondaryTransition: component.secondaryTransition, @@ -575,8 +603,6 @@ public final class ChatListNavigationBar: Component { } func update(component: ChatListNavigationBar, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { - let themeUpdated = self.component?.theme !== component.theme - var uploadProgressUpdated = false var storySubscriptionsUpdated = false if let previousComponent = self.component { @@ -591,23 +617,20 @@ public final class ChatListNavigationBar: Component { self.component = component self.state = state - if themeUpdated { - self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate) - self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor - } - var contentHeight = component.statusBarHeight if component.statusBarHeight >= 1.0 { contentHeight += 3.0 } contentHeight += 44.0 + contentHeight += 8.0 if component.isSearchActive { if component.statusBarHeight < 1.0 { contentHeight += 8.0 } - } else { + } + if component.search != nil { contentHeight += navigationBarSearchContentHeight } @@ -615,8 +638,11 @@ public final class ChatListNavigationBar: Component { contentHeight += 40.0 } - if component.accessoryPanelContainer != nil && !component.isSearchActive { - contentHeight += component.accessoryPanelContainerHeight + if component.isSearchActive { + } else { + if component.accessoryPanelContainer != nil { + contentHeight += component.accessoryPanelContainerHeight + } } let size = CGSize(width: availableSize.width, height: contentHeight) diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonComponent.swift new file mode 100644 index 0000000000..4027524c18 --- /dev/null +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonComponent.swift @@ -0,0 +1,254 @@ +import Foundation +import UIKit +import ComponentFlow +import ChatListTitleView +import TelegramPresentationData +import Display +import MoreHeaderButton + +public final class NavigationButtonComponentEnvironment: Equatable { + public let theme: PresentationTheme + + public init(theme: PresentationTheme) { + self.theme = theme + } + + public static func ==(lhs: NavigationButtonComponentEnvironment, rhs: NavigationButtonComponentEnvironment) -> Bool { + if lhs.theme != rhs.theme { + return false + } + return true + } +} + +public final class NavigationButtonComponent: Component { + public typealias EnvironmentType = NavigationButtonComponentEnvironment + + public enum Content: Equatable { + case text(title: String, isBold: Bool) + case more + case icon(imageName: String) + case proxy(status: ChatTitleProxyStatus) + } + + public let content: Content + public let pressed: (UIView) -> Void + public let contextAction: ((UIView, ContextGesture?) -> Void)? + + public init( + content: Content, + pressed: @escaping (UIView) -> Void, + contextAction: ((UIView, ContextGesture?) -> Void)? = nil + ) { + self.content = content + self.pressed = pressed + self.contextAction = contextAction + } + + public static func ==(lhs: NavigationButtonComponent, rhs: NavigationButtonComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + return true + } + + public final class View: HighlightTrackingButton { + private var textView: ImmediateTextView? + + private var iconView: UIImageView? + private var iconImageName: String? + + private var proxyNode: ChatTitleProxyNode? + + private var moreButton: MoreHeaderButton? + + private var component: NavigationButtonComponent? + private var theme: PresentationTheme? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.textView?.alpha = 0.6 + self.proxyNode?.alpha = 0.6 + self.iconView?.alpha = 0.6 + } else { + self.textView?.alpha = 1.0 + self.textView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + + self.proxyNode?.alpha = 1.0 + self.proxyNode?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + + self.iconView?.alpha = 1.0 + self.iconView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + } + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.component?.pressed(self) + } + + func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + self.component = component + + let theme = environment[NavigationButtonComponentEnvironment.self].value.theme + var themeUpdated = false + if self.theme !== theme { + self.theme = theme + themeUpdated = true + } + + var textString: NSAttributedString? + var imageName: String? + var proxyStatus: ChatTitleProxyStatus? + var isMore: Bool = false + + switch component.content { + case let .text(title, isBold): + textString = NSAttributedString(string: title, font: isBold ? Font.bold(17.0) : Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlColor) + case .more: + isMore = true + case let .icon(imageNameValue): + imageName = imageNameValue + case let .proxy(status): + proxyStatus = status + } + + var size = CGSize(width: 0.0, height: availableSize.height) + + if let textString = textString { + let textView: ImmediateTextView + if let current = self.textView { + textView = current + } else { + textView = ImmediateTextView() + textView.isUserInteractionEnabled = false + self.textView = textView + self.addSubview(textView) + } + + textView.attributedText = textString + let textSize = textView.updateLayout(availableSize) + let textInset: CGFloat = 10.0 + size.width = max(44.0, textSize.width + textInset * 2.0) + + textView.frame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: floor((availableSize.height - textSize.height) / 2.0)), size: textSize) + } else if let textView = self.textView { + self.textView = nil + textView.removeFromSuperview() + } + + if let imageName = imageName { + let iconView: UIImageView + if let current = self.iconView { + iconView = current + } else { + iconView = UIImageView() + iconView.isUserInteractionEnabled = false + self.iconView = iconView + self.addSubview(iconView) + } + if self.iconImageName != imageName || themeUpdated { + self.iconImageName = imageName + iconView.image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.chat.inputPanel.panelControlColor) + } + + if let iconSize = iconView.image?.size { + size.width = 44.0 + + iconView.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize) + } + } else if let iconView = self.iconView { + self.iconView = nil + iconView.removeFromSuperview() + self.iconImageName = nil + } + + if let proxyStatus = proxyStatus { + let proxyNode: ChatTitleProxyNode + if let current = self.proxyNode { + proxyNode = current + } else { + proxyNode = ChatTitleProxyNode(theme: theme) + proxyNode.isUserInteractionEnabled = false + self.proxyNode = proxyNode + self.addSubnode(proxyNode) + } + + let proxySize = CGSize(width: 30.0, height: 30.0) + size.width = 44.0 + + proxyNode.theme = theme + proxyNode.status = proxyStatus + + proxyNode.frame = CGRect(origin: CGPoint(x: floor((size.width - proxySize.width) / 2.0), y: floor((availableSize.height - proxySize.height) / 2.0)), size: proxySize) + } else if let proxyNode = self.proxyNode { + self.proxyNode = nil + proxyNode.removeFromSupernode() + } + + if isMore { + let moreButton: MoreHeaderButton + if let current = self.moreButton, !themeUpdated { + moreButton = current + } else { + if let moreButton = self.moreButton { + moreButton.removeFromSupernode() + self.moreButton = nil + } + + moreButton = MoreHeaderButton(color: theme.chat.inputPanel.panelControlColor) + moreButton.isUserInteractionEnabled = true + moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.chat.inputPanel.panelControlColor))) + moreButton.onPressed = { [weak self] in + guard let self, let component = self.component else { + return + } + self.moreButton?.play() + component.pressed(self) + } + moreButton.contextAction = { [weak self] sourceNode, gesture in + guard let self, let component = self.component else { + return + } + self.moreButton?.play() + component.contextAction?(self, gesture) + } + self.moreButton = moreButton + self.addSubnode(moreButton) + } + + let buttonSize = CGSize(width: 30.0, height: 44.0) + size.width = 44.0 + + moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) + + moreButton.frame = CGRect(origin: CGPoint(x: floor((size.width - buttonSize.width) / 2.0), y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) + } else if let moreButton = self.moreButton { + self.moreButton = nil + moreButton.removeFromSupernode() + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonContainerView.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonContainerView.swift new file mode 100644 index 0000000000..bdc1e32e4e --- /dev/null +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/NavigationButtonContainerView.swift @@ -0,0 +1,6 @@ +import Foundation +import UIKit +import ComponentFlow + +final class NavigationButtonContainerView: UIView { +} diff --git a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift index 5ec474b50d..b3d2606b47 100644 --- a/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift +++ b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift @@ -44,13 +44,13 @@ public final class ChatTitleProxyNode: ASDisplayNode { if self.theme !== oldValue { switch self.status { case .connecting: - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: false) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: false) case .connected: - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: true, off: false) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: true, off: false) case .available: - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: true) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: true) } - self.activityIndicator.type = .custom(theme.rootController.navigationBar.accentTextColor, 10.0, 1.3333, true) + self.activityIndicator.type = .custom(theme.chat.inputPanel.panelControlColor, 10.0, 1.3333, true) } } } @@ -61,13 +61,13 @@ public final class ChatTitleProxyNode: ASDisplayNode { switch self.status { case .connecting: self.activityIndicator.isHidden = false - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: false) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: false) case .connected: self.activityIndicator.isHidden = true - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: true, off: false) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: true, off: false) case .available: self.activityIndicator.isHidden = true - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: true) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: true) } } } @@ -80,9 +80,9 @@ public final class ChatTitleProxyNode: ASDisplayNode { self.iconNode.isLayerBacked = true self.iconNode.displayWithoutProcessing = true self.iconNode.displaysAsynchronously = false - self.iconNode.image = generateIcon(color: theme.rootController.navigationBar.accentTextColor, connected: false, off: true) + self.iconNode.image = generateIcon(color: theme.chat.inputPanel.panelControlColor, connected: false, off: true) - self.activityIndicator = ActivityIndicator(type: .custom(theme.rootController.navigationBar.accentTextColor, 10.0, 1.3333, true), speed: .slow) + self.activityIndicator = ActivityIndicator(type: .custom(theme.chat.inputPanel.panelControlColor, 10.0, 1.3333, true), speed: .slow) super.init() diff --git a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift index e5d1e84948..040181f130 100644 --- a/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift +++ b/submodules/TelegramUI/Components/LiquidLens/Sources/LiquidLensView.swift @@ -64,13 +64,15 @@ public final class LiquidLensView: UIView { var selectionWidth: CGFloat var isDark: Bool var isLifted: Bool + var isCollapsed: Bool - init(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool) { + init(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool) { self.size = size self.selectionX = selectionX self.selectionWidth = selectionWidth self.isLifted = isLifted self.isDark = isDark + self.isCollapsed = isCollapsed } } @@ -85,8 +87,8 @@ public final class LiquidLensView: UIView { } private let containerView: UIView - private let backgroundContainerContainer: UIView - private let backgroundContainer: GlassBackgroundContainerView + private let backgroundContainer: GlassBackgroundContainerView? + private let genericBackgroundContainer: UIView? private let backgroundView: GlassBackgroundView private var lensView: UIView? private let liftedContainerView: UIView @@ -117,11 +119,16 @@ public final class LiquidLensView: UIView { return self.params?.selectionWidth } - override public init(frame: CGRect) { + public init(useBackgroundContainer: Bool = true) { self.containerView = UIView() - self.backgroundContainerContainer = UIView() - self.backgroundContainer = GlassBackgroundContainerView() + if useBackgroundContainer { + self.backgroundContainer = GlassBackgroundContainerView() + self.genericBackgroundContainer = nil + } else { + self.backgroundContainer = nil + self.genericBackgroundContainer = UIView() + } self.backgroundView = GlassBackgroundView() @@ -130,12 +137,16 @@ public final class LiquidLensView: UIView { self.restingBackgroundView = RestingBackgroundView() - super.init(frame: frame) + super.init(frame: CGRect()) - self.backgroundContainerContainer.addSubview(self.backgroundContainer) - self.addSubview(self.backgroundContainerContainer) + if let backgroundContainer = self.backgroundContainer { + self.addSubview(backgroundContainer) + backgroundContainer.contentView.addSubview(self.backgroundView) + } else if let genericBackgroundContainer = self.genericBackgroundContainer { + self.addSubview(genericBackgroundContainer) + genericBackgroundContainer.addSubview(self.backgroundView) + } - self.backgroundContainer.contentView.addSubview(self.backgroundView) self.backgroundView.contentView.addSubview(self.containerView) self.containerView.isUserInteractionEnabled = false @@ -150,7 +161,11 @@ public final class LiquidLensView: UIView { } if let lensView = self.lensView { - self.backgroundContainer.layer.zPosition = 1 + if let backgroundContainer = self.backgroundContainer { + backgroundContainer.layer.zPosition = 1 + } else if let genericBackgroundContainer = self.genericBackgroundContainer{ + genericBackgroundContainer.layer.zPosition = 1 + } lensView.layer.zPosition = 10.0 self.liftedContainerView.addSubview(self.restingBackgroundView) @@ -159,7 +174,11 @@ public final class LiquidLensView: UIView { self.containerView.addSubview(lensView) self.containerView.addSubview(self.contentView) - lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: self.backgroundContainer.contentView) + if let backgroundContainer = self.backgroundContainer { + lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: backgroundContainer.contentView) + } else if let genericBackgroundContainer = self.genericBackgroundContainer { + lensView.perform(NSSelectorFromString("setLiftedContainerView:"), with: genericBackgroundContainer) + } lensView.perform(NSSelectorFromString("setLiftedContentView:"), with: self.liftedContainerView) lensView.perform(NSSelectorFromString("setOverridePunchoutView:"), with: self.contentView) @@ -223,8 +242,8 @@ public final class LiquidLensView: UIView { fatalError("init(coder:) has not been implemented") } - public func update(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool, transition: ComponentTransition) { - let params = Params(size: size, selectionX: selectionX, selectionWidth: selectionWidth, isDark: isDark, isLifted: isLifted) + public func update(size: CGSize, selectionX: CGFloat, selectionWidth: CGFloat, isDark: Bool, isLifted: Bool, isCollapsed: Bool = false, transition: ComponentTransition) { + let params = Params(size: size, selectionX: selectionX, selectionWidth: selectionWidth, isDark: isDark, isLifted: isLifted, isCollapsed: isCollapsed) if self.params == params { return } @@ -319,16 +338,36 @@ public final class LiquidLensView: UIView { self.params = params transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(), size: params.size)) - transition.setFrame(view: self.backgroundContainerContainer, frame: CGRect(origin: CGPoint(), size: params.size)) - transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size)) - self.backgroundContainer.update(size: params.size, isDark: params.isDark, transition: transition) + if let backgroundContainer = self.backgroundContainer { + transition.setFrame(view: backgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size)) + backgroundContainer.update(size: params.size, isDark: params.isDark, transition: transition) + } else if let genericBackgroundContainer = self.genericBackgroundContainer { + transition.setFrame(view: genericBackgroundContainer, frame: CGRect(origin: CGPoint(), size: params.size)) + } transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) self.backgroundView.update(size: params.size, cornerRadius: params.size.height * 0.5, isDark: params.isDark, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.isDark ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) - transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: params.size)) - transition.setFrame(view: self.liftedContainerView, frame: CGRect(origin: CGPoint(), size: params.size)) + if self.contentView.bounds.size != params.size { + self.contentView.clipsToBounds = true + transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: params.size), completion: { [weak self] completed in + guard let self, completed else { + return + } + self.contentView.clipsToBounds = false + }) + transition.setCornerRadius(layer: self.contentView.layer, cornerRadius: params.size.height * 0.5) + + self.liftedContainerView.clipsToBounds = true + transition.setFrame(view: self.liftedContainerView, frame: CGRect(origin: CGPoint(), size: params.size), completion: { [weak self] completed in + guard let self, completed else { + return + } + self.liftedContainerView.clipsToBounds = false + }) + transition.setCornerRadius(layer: self.liftedContainerView.layer, cornerRadius: params.size.height * 0.5) + } let baseLensFrame = CGRect(origin: CGPoint(x: max(0.0, min(params.selectionX, params.size.width - params.selectionWidth)), y: 0.0), size: CGSize(width: params.selectionWidth, height: params.size.height)) self.updateLens(params: LensParams(baseFrame: baseLensFrame, isLifted: params.isLifted), animated: !transition.animation.isImmediate) @@ -354,7 +393,7 @@ public final class LiquidLensView: UIView { transition.setFrame(view: self.restingBackgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) self.restingBackgroundView.update(isDark: params.isDark) - transition.setAlpha(view: self.restingBackgroundView, alpha: params.isLifted ? 0.0 : 1.0) + transition.setAlpha(view: self.restingBackgroundView, alpha: (params.isLifted || params.isCollapsed) ? 0.0 : 1.0) if params.isLifted { if self.liftedDisplayLink == nil { diff --git a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift index 579bd7b8d9..5cb901cfde 100644 --- a/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift +++ b/submodules/TelegramUI/Components/MiniAppListScreen/Sources/MiniAppListScreen.swift @@ -266,8 +266,7 @@ final class MiniAppListScreenComponent: Component { } ))) : nil, rightButtons: rightButtons, - backTitle: isModal ? nil : strings.Common_Back, - backPressed: { [weak self] in + backPressed: isModal ? nil : { [weak self] in guard let self else { return } @@ -286,8 +285,8 @@ final class MiniAppListScreenComponent: Component { strings: strings, statusBarHeight: statusBarHeight, sideInset: insets.left, + search: ChatListNavigationBar.Search(isEnabled: true), isSearchActive: self.isSearchDisplayControllerActive, - isSearchEnabled: true, primaryContent: headerContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index ad318df6ff..c05923ba09 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -13923,6 +13923,8 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc } self._readyProxy.set(.single(true)) }) + + self.updateTabBarSearchState(ViewController.TabBarSearchState(isActive: false), transition: .immediate) } required init(coder aDecoder: NSCoder) { @@ -14476,7 +14478,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig if let topNavigationBar = self.topNavigationBar, let bottomNavigationBar = self.bottomNavigationBar { self.addSubnode(bottomNavigationBar.additionalContentNode) - if let headerView = bottomNavigationBar.customHeaderContentView as? ChatListHeaderComponent.View { + /*if let headerView = bottomNavigationBar.customHeaderContentView as? ChatListHeaderComponent.View { if let previousBackButtonArrow = headerView.makeTransitionBackArrowView(accentColor: self.presentationData.theme.rootController.navigationBar.accentTextColor) { self.previousBackButtonArrow = previousBackButtonArrow self.view.addSubview(previousBackButtonArrow) @@ -14485,7 +14487,7 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig self.previousBackButton = previousBackButton self.view.addSubview(previousBackButton) } - } else { + } else*/ do { if let previousBackButtonArrow = bottomNavigationBar.makeTransitionBackArrowView(accentColor: self.presentationData.theme.rootController.navigationBar.accentTextColor) { self.previousBackButtonArrow = previousBackButtonArrow self.view.addSubview(previousBackButtonArrow) @@ -14594,13 +14596,13 @@ private final class PeerInfoNavigationTransitionNode: ASDisplayNode, CustomNavig } if let previousBackButton = self.previousBackButton { - if let headerView = bottomNavigationBar.customHeaderContentView as? ChatListHeaderComponent.View { + /*if let headerView = bottomNavigationBar.customHeaderContentView as? ChatListHeaderComponent.View { if let backButtonTitleView = headerView.backButtonTitleView { let previousBackButtonFrame = backButtonTitleView.convert(backButtonTitleView.bounds, to: bottomNavigationBar.view) previousBackButton.frame = previousBackButtonFrame transition.updateAlpha(layer: previousBackButton.layer, alpha: fraction) } - } else { + } else*/ do { let previousBackButtonFrame = bottomNavigationBar.backButtonNode.view.convert(bottomNavigationBar.backButtonNode.view.bounds, to: bottomNavigationBar.view) previousBackButton.frame = previousBackButtonFrame transition.updateAlpha(layer: previousBackButton.layer, alpha: fraction) diff --git a/submodules/TelegramUI/Components/PeerSelectionController/BUILD b/submodules/TelegramUI/Components/PeerSelectionController/BUILD index 230b96365d..f2c10542eb 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/BUILD +++ b/submodules/TelegramUI/Components/PeerSelectionController/BUILD @@ -35,6 +35,7 @@ swift_library( "//submodules/TextFormat", "//submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode", "//submodules/CounterControllerTitleView", + "//submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift index db095daadd..0dc5f6b517 100644 --- a/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift +++ b/submodules/TelegramUI/Components/PeerSelectionController/Sources/PeerSelectionController.swift @@ -10,6 +10,7 @@ import AccountContext import SearchUI import ChatListUI import CounterControllerTitleView +import ChatListFilterTabContainerNode public final class PeerSelectionControllerImpl: ViewController, PeerSelectionController { private let context: AccountContext diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift index 67c0665476..0114593bb9 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/QuickReplySetupScreen.swift @@ -798,8 +798,7 @@ final class QuickReplySetupScreenComponent: Component { } ))) : nil, rightButtons: rightButtons, - backTitle: isModal ? nil : strings.Common_Back, - backPressed: { [weak self] in + backPressed: isModal ? nil :{ [weak self] in guard let self else { return } @@ -818,8 +817,8 @@ final class QuickReplySetupScreenComponent: Component { strings: strings, statusBarHeight: statusBarHeight, sideInset: insets.left, + search: ChatListNavigationBar.Search(isEnabled: !self.isEditing), isSearchActive: self.isSearchDisplayControllerActive, - isSearchEnabled: !self.isEditing, primaryContent: headerContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift index b99ea5e371..8aadf7da54 100644 --- a/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift +++ b/submodules/TelegramUI/Components/Settings/PeerSelectionScreen/Sources/PeerSelectionScreen.swift @@ -298,8 +298,7 @@ final class PeerSelectionScreenComponent: Component { } ))) : nil, rightButtons: rightButtons, - backTitle: isModal ? nil : strings.Common_Back, - backPressed: { [weak self] in + backPressed: isModal ? nil : { [weak self] in guard let self else { return } @@ -318,8 +317,8 @@ final class PeerSelectionScreenComponent: Component { strings: strings, statusBarHeight: statusBarHeight, sideInset: insets.left, + search: ChatListNavigationBar.Search(isEnabled: true), isSearchActive: self.isSearchDisplayControllerActive, - isSearchEnabled: true, primaryContent: headerContent, secondaryContent: nil, secondaryTransition: 0.0, diff --git a/submodules/TelegramUI/Components/TabBarComponent/BUILD b/submodules/TelegramUI/Components/TabBarComponent/BUILD index bf1656c7b3..e08cc9e22b 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/BUILD +++ b/submodules/TelegramUI/Components/TabBarComponent/BUILD @@ -22,6 +22,7 @@ swift_library( "//submodules/UIKitRuntimeUtils", "//submodules/TelegramUI/Components/LiquidLens", "//submodules/AppBundle", + "//submodules/SearchBarNode", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift index 585e15ed8c..010a270d59 100644 --- a/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift +++ b/submodules/TelegramUI/Components/TabBarComponent/Sources/TabBarComponent.swift @@ -12,6 +12,7 @@ import BundleIconComponent import TextBadgeComponent import LiquidLens import AppBundle +import SearchBarNode private final class TabSelectionRecognizer: UIGestureRecognizer { private var initialLocation: CGPoint? @@ -69,35 +70,147 @@ private final class TabSelectionRecognizer: UIGestureRecognizer { } } -public final class TabBarSearchView: UIView { - private let backgroundView: GlassBackgroundView - private let iconView: GlassBackgroundView.ContentImageView - - override public init(frame: CGRect) { - self.backgroundView = GlassBackgroundView() - self.iconView = GlassBackgroundView.ContentImageView() +public final class NavigationSearchView: UIView { + private struct Params: Equatable { + let size: CGSize + let theme: PresentationTheme + let strings: PresentationStrings + let isActive: Bool - super.init(frame: frame) + init(size: CGSize, theme: PresentationTheme, strings: PresentationStrings, isActive: Bool) { + self.size = size + self.theme = theme + self.strings = strings + self.isActive = isActive + } + + static func ==(lhs: Params, rhs: Params) -> Bool { + if lhs.size != rhs.size { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.isActive != rhs.isActive { + return false + } + return true + } + } + + private let action: () -> Void + + private let backgroundView: GlassBackgroundView + private let iconView: UIImageView + private(set) var searchBarNode: SearchBarNode? + + private var params: Params? + + public init(action: @escaping () -> Void) { + self.action = action + + self.backgroundView = GlassBackgroundView() + self.backgroundView.contentView.clipsToBounds = true + self.iconView = UIImageView() + + super.init(frame: CGRect()) self.addSubview(self.backgroundView) self.backgroundView.contentView.addSubview(self.iconView) + + self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTapGesture(_:)))) + } + + @objc private func onTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.action() + } } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - public func update(size: CGSize, isDark: Bool, tintColor: GlassBackgroundView.TintColor, iconColor: UIColor, transition: ComponentTransition) { - transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) - self.backgroundView.update(size: size, cornerRadius: size.height * 0.5, isDark: isDark, tintColor: tintColor, transition: transition) + public func update(size: CGSize, theme: PresentationTheme, strings: PresentationStrings, isActive: Bool, transition: ComponentTransition) { + let params = Params(size: size, theme: theme, strings: strings, isActive: isActive) + if self.params == params { + return + } + self.params = params + self.update(params: params, transition: transition) + } + + private func update(params: Params, transition: ComponentTransition) { + transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: params.size)) + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) + + self.backgroundView.update(size: params.size, cornerRadius: params.size.height * 0.5, isDark: params.theme.overallDarkAppearance, tintColor: GlassBackgroundView.TintColor.init(kind: .panel, color: UIColor(white: params.theme.overallDarkAppearance ? 0.0 : 1.0, alpha: 0.6)), isInteractive: true, transition: transition) if self.iconView.image == nil { self.iconView.image = UIImage(bundleImageName: "Navigation/Search")?.withRenderingMode(.alwaysTemplate) } - self.iconView.tintColor = iconColor + transition.setTintColor(view: self.iconView, color: params.isActive ? params.theme.rootController.navigationSearchBar.inputIconColor : params.theme.chat.inputPanel.panelControlColor) if let image = self.iconView.image { - transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), size: image.size)) + let imageSize: CGSize + let iconFrame: CGRect + if params.isActive { + let iconFraction: CGFloat = 0.8 + imageSize = CGSize(width: image.size.width * iconFraction, height: image.size.height * iconFraction) + iconFrame = CGRect(origin: CGPoint(x: 12.0, y: floor((params.size.height - imageSize.height) * 0.5)), size: imageSize) + } else { + iconFrame = CGRect(origin: CGPoint(x: floor((params.size.width - image.size.width) * 0.5), y: floor((params.size.height - image.size.height) * 0.5)), size: image.size) + } + transition.setPosition(view: self.iconView, position: iconFrame.center) + transition.setBounds(view: self.iconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) + } + + if params.isActive { + let searchBarNode: SearchBarNode + var searchBarNodeTransition = transition + if let current = self.searchBarNode { + searchBarNode = current + } else { + searchBarNodeTransition = searchBarNodeTransition.withAnimation(.none) + searchBarNode = SearchBarNode( + theme: SearchBarNodeTheme( + background: .clear, + separator: .clear, + inputFill: .clear, + primaryText: params.theme.chat.inputPanel.panelControlColor, + placeholder: params.theme.chat.inputPanel.inputPlaceholderColor, + inputIcon: params.theme.chat.inputPanel.inputControlColor, + inputClear: params.theme.chat.inputPanel.inputControlColor, + accent: params.theme.chat.inputPanel.panelControlAccentColor, + keyboard: params.theme.rootController.keyboardColor + ), + strings: params.strings, + fieldStyle: .inlineNavigation, + icon: .loupe, + forceSeparator: false, + displayBackground: false, + cancelText: nil + ) + searchBarNode.placeholderString = NSAttributedString(string: params.strings.Common_Search, font: Font.regular(17.0), textColor: params.theme.chat.inputPanel.inputPlaceholderColor) + self.searchBarNode = searchBarNode + self.backgroundView.contentView.addSubview(searchBarNode.view) + searchBarNode.view.alpha = 0.0 + } + let searchBarFrame = CGRect(origin: CGPoint(x: 36.0, y: 0.0), size: CGSize(width: params.size.width - 36.0 - 4.0, height: params.size.height)) + transition.setFrame(view: searchBarNode.view, frame: searchBarFrame) + searchBarNode.updateLayout(boundingSize: searchBarFrame.size, leftInset: 0.0, rightInset: 0.0, transition: transition.containedViewLayoutTransition) + alphaTransition.setAlpha(view: searchBarNode.view, alpha: 1.0) + } else { + if let searchBarNode = self.searchBarNode { + self.searchBarNode = nil + let searchBarNodeView = searchBarNode.view + alphaTransition.setAlpha(view: searchBarNode.view, alpha: 0.0, completion: { [weak searchBarNodeView] _ in + searchBarNodeView?.removeFromSuperview() + }) + } } } } @@ -131,20 +244,45 @@ public final class TabBarComponent: Component { return true } } + + public final class Search: Equatable { + public let isActive: Bool + public let activate: () -> Void + public let deactivate: () -> Void + + public init(isActive: Bool, activate: @escaping () -> Void, deactivate: @escaping () -> Void) { + self.isActive = isActive + self.activate = activate + self.deactivate = deactivate + } + + public static func ==(lhs: Search, rhs: Search) -> Bool { + if lhs.isActive != rhs.isActive { + return false + } + return true + } + } public let theme: PresentationTheme + public let strings: PresentationStrings public let items: [Item] + public let search: Search? public let selectedId: AnyHashable? public let isTablet: Bool public init( theme: PresentationTheme, + strings: PresentationStrings, items: [Item], + search: Search?, selectedId: AnyHashable?, isTablet: Bool ) { self.theme = theme + self.strings = strings self.items = items + self.search = search self.selectedId = selectedId self.isTablet = isTablet } @@ -153,9 +291,15 @@ public final class TabBarComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.strings !== rhs.strings { + return false + } if lhs.items != rhs.items { return false } + if lhs.search != rhs.search { + return false + } if lhs.selectedId != rhs.selectedId { return false } @@ -166,11 +310,14 @@ public final class TabBarComponent: Component { } public final class View: UIView, UITabBarDelegate, UIGestureRecognizerDelegate { + private let backgroundContainer: GlassBackgroundContainerView private let liquidLensView: LiquidLensView private let contextGestureContainerView: ContextControllerSourceView private var itemViews: [AnyHashable: ComponentView] = [:] private var selectedItemViews: [AnyHashable: ComponentView] = [:] + + private var searchView: NavigationSearchView? private var tabSelectionRecognizer: TabSelectionRecognizer? private var itemWithActiveContextGesture: AnyHashable? @@ -178,11 +325,16 @@ public final class TabBarComponent: Component { private var component: TabBarComponent? private weak var state: EmptyComponentState? - private var selectionGestureState: (startX: CGFloat, currentX: CGFloat)? + private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)? private var overrideSelectedItemId: AnyHashable? + + public var currentSearchNode: ASDisplayNode? { + return self.searchView?.searchBarNode + } public override init(frame: CGRect) { - self.liquidLensView = LiquidLensView() + self.backgroundContainer = GlassBackgroundContainerView() + self.liquidLensView = LiquidLensView(useBackgroundContainer: false) self.contextGestureContainerView = ContextControllerSourceView() self.contextGestureContainerView.isGestureEnabled = true @@ -194,12 +346,13 @@ public final class TabBarComponent: Component { self.traitOverrides.horizontalSizeClass = .compact } - self.addSubview(self.contextGestureContainerView) + self.addSubview(self.backgroundContainer) + self.backgroundContainer.contentView.addSubview(self.contextGestureContainerView) self.contextGestureContainerView.addSubview(self.liquidLensView) let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:))) self.tabSelectionRecognizer = tabSelectionRecognizer - self.addGestureRecognizer(tabSelectionRecognizer) + self.contextGestureContainerView.addGestureRecognizer(tabSelectionRecognizer) self.contextGestureContainerView.shouldBegin = { [weak self] point in guard let self, let component = self.component else { @@ -281,29 +434,41 @@ public final class TabBarComponent: Component { } @objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) { + guard let component = self.component else { + return + } switch recognizer.state { case .began: - if let itemId = self.item(at: recognizer.location(in: self)), let itemView = self.itemViews[itemId]?.view { + if let search = component.search, search.isActive { + } else if let itemId = self.item(at: recognizer.location(in: self)), let itemView = self.itemViews[itemId]?.view { let startX = itemView.frame.minX - 4.0 - self.selectionGestureState = (startX, startX) + self.selectionGestureState = (startX, startX, itemId) self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) } case .changed: - if var selectionGestureState = self.selectionGestureState { + if let search = component.search, search.isActive { + } else if var selectionGestureState = self.selectionGestureState { selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x + if let itemId = self.item(at: recognizer.location(in: self)) { + selectionGestureState.itemId = itemId + } self.selectionGestureState = selectionGestureState self.state?.updated(transition: .immediate, isLocal: true) } case .ended, .cancelled: - self.selectionGestureState = nil - if let component = self.component, let itemId = self.item(at: recognizer.location(in: self)) { - guard let item = component.items.first(where: { $0.id == itemId }) else { - return + if let search = component.search, search.isActive { + search.deactivate() + } else if let selectionGestureState = self.selectionGestureState { + self.selectionGestureState = nil + if case .ended = recognizer.state, let component = self.component { + guard let item = component.items.first(where: { $0.id == selectionGestureState.itemId }) else { + return + } + self.overrideSelectedItemId = selectionGestureState.itemId + item.action(false) } - self.overrideSelectedItemId = itemId - item.action(false) + self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) } - self.state?.updated(transition: .spring(duration: 0.4), isLocal: true) default: break } @@ -355,22 +520,28 @@ public final class TabBarComponent: Component { } func update(component: TabBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) + let _ = alphaTransition + let innerInset: CGFloat = 4.0 let availableSize = CGSize(width: min(500.0, availableSize.width), height: availableSize.height) let previousComponent = self.component self.component = component self.state = state - - let _ = innerInset - let _ = availableSize - let _ = previousComponent self.overrideUserInterfaceStyle = component.theme.overallDarkAppearance ? .dark : .light - let itemSize = CGSize(width: floor((availableSize.width - innerInset * 2.0) / CGFloat(component.items.count)), height: 56.0) + let barHeight: CGFloat = 56.0 + innerInset * 2.0 + + var availableItemsWidth: CGFloat = availableSize.width - innerInset * 2.0 + if component.search != nil { + availableItemsWidth -= barHeight + 8.0 + } + + let itemSize = CGSize(width: floor(availableItemsWidth / CGFloat(component.items.count)), height: 56.0) let contentWidth: CGFloat = innerInset * 2.0 + CGFloat(component.items.count) * itemSize.width - let size = CGSize(width: min(availableSize.width, contentWidth), height: itemSize.height + innerInset * 2.0) + let tabsSize = CGSize(width: min(availableSize.width, contentWidth), height: itemSize.height + innerInset * 2.0) var validIds: [AnyHashable] = [] var selectionFrame: CGRect? @@ -409,6 +580,7 @@ public final class TabBarComponent: Component { component: AnyComponent(ItemComponent( item: item, theme: component.theme, + isCompact: component.search?.isActive == true, isSelected: false )), environment: {}, @@ -419,14 +591,21 @@ public final class TabBarComponent: Component { component: AnyComponent(ItemComponent( item: item, theme: component.theme, + isCompact: component.search?.isActive == true, isSelected: true )), environment: {}, containerSize: itemSize ) - let itemFrame = CGRect(origin: CGPoint(x: innerInset + CGFloat(index) * itemSize.width, y: floor((size.height - itemSize.height) * 0.5)), size: itemSize) + var itemFrame = CGRect(origin: CGPoint(x: innerInset + CGFloat(index) * itemSize.width, y: floor((tabsSize.height - itemSize.height) * 0.5)), size: itemSize) + if isItemSelected { + selectionFrame = itemFrame + } + if let itemComponentView = itemView.view as? ItemComponent.View, let selectedItemComponentView = selectedItemView.view as? ItemComponent.View { + let itemAlphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) + if itemComponentView.superview == nil { itemComponentView.isUserInteractionEnabled = false selectedItemComponentView.isUserInteractionEnabled = false @@ -434,6 +613,27 @@ public final class TabBarComponent: Component { self.liquidLensView.contentView.addSubview(itemComponentView) self.liquidLensView.selectedContentView.addSubview(selectedItemComponentView) } + + if let search = component.search, search.isActive { + if isItemSelected { + itemFrame.origin.x = floor((48.0 - itemSize.width) * 0.5) + itemTransition.setAlpha(view: itemComponentView, alpha: 1.0) + itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 0.0) + itemTransition.setAlpha(view: selectedItemComponentView, alpha: 1.0) + itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 0.0) + } else { + itemTransition.setAlpha(view: itemComponentView, alpha: 0.0) + itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 10.0) + itemTransition.setAlpha(view: selectedItemComponentView, alpha: 0.0) + itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 10.0) + } + } else { + itemTransition.setAlpha(view: itemComponentView, alpha: 1.0) + itemAlphaTransition.setBlur(layer: itemComponentView.layer, radius: 0.0) + itemTransition.setAlpha(view: selectedItemComponentView, alpha: 1.0) + itemAlphaTransition.setBlur(layer: selectedItemComponentView.layer, radius: 0.0) + } + itemTransition.setFrame(view: itemComponentView, frame: itemFrame) itemTransition.setPosition(view: selectedItemComponentView, position: itemFrame.center) itemTransition.setBounds(view: selectedItemComponentView, bounds: CGRect(origin: CGPoint(), size: itemFrame.size)) @@ -444,9 +644,6 @@ public final class TabBarComponent: Component { selectedItemComponentView.playSelectionAnimation() } } - if isItemSelected { - selectionFrame = itemFrame - } } var removeIds: [AnyHashable] = [] @@ -461,12 +658,17 @@ public final class TabBarComponent: Component { self.itemViews.removeValue(forKey: id) self.selectedItemViews.removeValue(forKey: id) } - - transition.setFrame(view: self.contextGestureContainerView, frame: CGRect(origin: CGPoint(), size: size)) - - transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(), size: size)) - let lensSelection: (x: CGFloat, width: CGFloat) + var tabsFrame = CGRect(origin: CGPoint(), size: tabsSize) + if let search = component.search, search.isActive { + tabsFrame.size = CGSize(width: 48.0, height: 48.0) + tabsFrame.origin.y = tabsSize.height - 48.0 + } + + transition.setFrame(view: self.contextGestureContainerView, frame: tabsFrame) + transition.setFrame(view: self.liquidLensView, frame: CGRect(origin: CGPoint(), size: tabsSize)) + + var lensSelection: (x: CGFloat, width: CGFloat) if let selectionGestureState = self.selectionGestureState { lensSelection = (selectionGestureState.currentX, itemSize.width + innerInset * 2.0) } else if let selectionFrame { @@ -475,7 +677,62 @@ public final class TabBarComponent: Component { lensSelection = (0.0, itemSize.width) } - self.liquidLensView.update(size: size, selectionX: lensSelection.x, selectionWidth: lensSelection.width, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, transition: transition) + var lensSize: CGSize = tabsSize + var isLensCollapsed = false + if let search = component.search, search.isActive { + isLensCollapsed = true + lensSize = CGSize(width: 48.0, height: 48.0) + lensSelection = (0.0, 48.0) + } + self.liquidLensView.update(size: lensSize, selectionX: lensSelection.x, selectionWidth: lensSelection.width, isDark: component.theme.overallDarkAppearance, isLifted: self.selectionGestureState != nil, isCollapsed: isLensCollapsed, transition: transition) + + var size = tabsSize + + if let search = component.search { + let searchSize: CGSize + let searchFrame: CGRect + if search.isActive { + size.width = availableSize.width + searchSize = CGSize(width: availableSize.width - 48.0 - 8.0, height: 48.0) + searchFrame = CGRect(origin: CGPoint(x: 48.0 + 8.0, y: size.height - searchSize.height), size: searchSize) + } else { + searchSize = CGSize(width: barHeight, height: barHeight) + size.width += barHeight + 8.0 + searchFrame = CGRect(origin: CGPoint(x: availableSize.width - searchSize.width, y: 0.0), size: searchSize) + } + + let searchView: NavigationSearchView + var searchViewTransition = transition + if let current = self.searchView { + searchView = current + } else { + searchViewTransition = searchViewTransition.withAnimation(.none) + searchView = NavigationSearchView(action: { [weak self] in + guard let self, let component = self.component else { + return + } + component.search?.activate() + }) + self.searchView = searchView + self.backgroundContainer.contentView.addSubview(searchView) + searchView.frame = CGRect(origin: CGPoint(x: availableSize.width + 50.0, y: 0.0), size: searchSize) + } + searchView.update(size: searchSize, theme: component.theme, strings: component.strings, isActive: search.isActive, transition: searchViewTransition) + transition.setFrame(view: searchView, frame: searchFrame) + } else { + if let searchView = self.searchView { + self.searchView = nil + transition.setFrame(view: searchView, frame: CGRect(origin: CGPoint(x: availableSize.width + 50.0, y: 0.0), size: searchView.bounds.size), completion: { [weak searchView] completed in + guard let searchView, completed else { + return + } + searchView.removeFromSuperview() + }) + } + } + + transition.setFrame(view: self.backgroundContainer, frame: CGRect(origin: CGPoint(), size: size)) + self.backgroundContainer.update(size: size, isDark: component.theme.overallDarkAppearance, transition: transition) return size } @@ -493,11 +750,13 @@ public final class TabBarComponent: Component { private final class ItemComponent: Component { let item: TabBarComponent.Item let theme: PresentationTheme + let isCompact: Bool let isSelected: Bool - init(item: TabBarComponent.Item, theme: PresentationTheme, isSelected: Bool) { + init(item: TabBarComponent.Item, theme: PresentationTheme, isCompact: Bool, isSelected: Bool) { self.item = item self.theme = theme + self.isCompact = isCompact self.isSelected = isSelected } @@ -508,6 +767,9 @@ private final class ItemComponent: Component { if lhs.theme !== rhs.theme { return false } + if lhs.isCompact != rhs.isCompact { + return false + } if lhs.isSelected != rhs.isSelected { return false } @@ -562,6 +824,8 @@ private final class ItemComponent: Component { } func update(component: ItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { + let alphaTransition: ComponentTransition = transition.animation.isImmediate ? .immediate : .easeInOut(duration: 0.25) + let previousComponent = self.component if previousComponent?.item.item !== component.item.item { @@ -619,7 +883,7 @@ private final class ItemComponent: Component { content: LottieComponent.AppBundleContent( name: animationName ), - color: component.isSelected ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor, + color: (component.isSelected && !component.isCompact) ? component.theme.rootController.tabBar.selectedTextColor : component.theme.rootController.tabBar.textColor, placeholderColor: nil, startingPosition: .end, size: CGSize(width: 48.0, height: 48.0), @@ -692,6 +956,7 @@ private final class ItemComponent: Component { self.contextContainerView.contentView.addSubview(titleView) } titleView.frame = titleFrame + alphaTransition.setAlpha(view: titleView, alpha: component.isCompact ? 0.0 : 1.0) } if let badgeText = component.item.item.badgeValue, !badgeText.isEmpty { @@ -723,6 +988,7 @@ private final class ItemComponent: Component { self.contextContainerView.contentView.addSubview(badgeView) } badgeTransition.setFrame(view: badgeView, frame: badgeFrame) + alphaTransition.setAlpha(view: badgeView, alpha: component.isCompact ? 0.0 : 1.0) } } else if let badge = self.badge { self.badge = nil diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index c0f1427eee..2abea859eb 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -188,7 +188,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon } public func addRootControllers(showCallsTab: Bool) { - let tabBarController = TabBarControllerImpl(theme: self.presentationData.theme) + let tabBarController = TabBarControllerImpl(theme: self.presentationData.theme, strings: self.presentationData.strings) tabBarController.navigationPresentation = .master let chatListController = self.context.sharedContext.makeChatListController(context: self.context, location: .chatList(groupId: .root), controlsHistoryPreload: true, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: !GlobalExperimentalSettings.isAppStoreBuild) if let sharedContext = self.context.sharedContext as? SharedAccountContextImpl {