Revert "Revert "[WIP] Tab bar and search""

This reverts commit 89f96e28b3.
This commit is contained in:
Isaac 2025-12-02 17:30:30 +08:00
parent 6dba110767
commit 8047d0fa2a
33 changed files with 1474 additions and 816 deletions

View file

@ -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",

View file

@ -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) {

View file

@ -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",

View file

@ -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() {

View file

@ -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))

View file

@ -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 {

View file

@ -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

View file

@ -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) {

View file

@ -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,

View file

@ -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?) {
}

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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",
],
)

View file

@ -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
}

View file

@ -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",

View file

@ -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<NavigationButtonComponentEnvironment>?
public let rightButtons: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>]
public let backTitle: String?
public let backPressed: (() -> Void)?
public init(
@ -89,7 +89,6 @@ public final class ChatListHeaderComponent: Component {
chatListTitle: NetworkStatusTitle?,
leftButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>?,
rightButtons: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>],
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<NavigationButtonComponentEnvironment>] = [:]
let rightButtonOffsetContainer: UIView
let rightButtonsContainer: UIView
var rightButtonViews: [AnyHashable: ComponentView<NavigationButtonComponentEnvironment>] = [:]
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<AnyHashable>()
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<AnyHashable>()
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<Empty>?
@ -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<NavigationButtonComponentEnvironment>, 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<NavigationButtonComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -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<Empty>()
@ -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<Empty>, 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)

View file

@ -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<NavigationButtonComponentEnvironment>, 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<NavigationButtonComponentEnvironment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View file

@ -0,0 +1,6 @@
import Foundation
import UIKit
import ComponentFlow
final class NavigationButtonContainerView: UIView {
}

View file

@ -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()

View file

@ -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 {

View file

@ -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,

View file

@ -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)

View file

@ -35,6 +35,7 @@ swift_library(
"//submodules/TextFormat",
"//submodules/TelegramUI/Components/Chat/ForwardAccessoryPanelNode",
"//submodules/CounterControllerTitleView",
"//submodules/TelegramUI/Components/ChatList/ChatListFilterTabContainerNode",
],
visibility = [
"//visibility:public",

View file

@ -10,6 +10,7 @@ import AccountContext
import SearchUI
import ChatListUI
import CounterControllerTitleView
import ChatListFilterTabContainerNode
public final class PeerSelectionControllerImpl: ViewController, PeerSelectionController {
private let context: AccountContext

View file

@ -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,

View file

@ -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,

View file

@ -22,6 +22,7 @@ swift_library(
"//submodules/UIKitRuntimeUtils",
"//submodules/TelegramUI/Components/LiquidLens",
"//submodules/AppBundle",
"//submodules/SearchBarNode",
],
visibility = [
"//visibility:public",

View file

@ -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<Empty>] = [:]
private var selectedItemViews: [AnyHashable: ComponentView<Empty>] = [:]
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<Empty>, 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<Empty>, 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

View file

@ -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 {