import Foundation import UIKit import Display import ComponentFlow import TelegramPresentationData import AccountContext import ChatListTitleView import AppBundle import StoryPeerListComponent import TelegramCore public final class HeaderNetworkStatusComponent: Component { public enum Content: Equatable { case connecting case updating } public let content: Content public let theme: PresentationTheme public let strings: PresentationStrings public init( content: Content, theme: PresentationTheme, strings: PresentationStrings ) { self.content = content self.theme = theme self.strings = strings } public static func ==(lhs: HeaderNetworkStatusComponent, rhs: HeaderNetworkStatusComponent) -> Bool { if lhs.content != rhs.content { return false } if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } return true } public final class View: UIView { private var component: HeaderNetworkStatusComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { super.init(frame: frame) } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func update(component: HeaderNetworkStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.state = state return availableSize } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } public final class ChatListHeaderComponent: Component { public final class Content: Equatable { public let title: String public let navigationBackTitle: String? public let titleComponent: AnyComponent? public let chatListTitle: NetworkStatusTitle? public let leftButton: AnyComponentWithIdentity? public let rightButtons: [AnyComponentWithIdentity] public let backTitle: String? public let backPressed: (() -> Void)? public init( title: String, navigationBackTitle: String?, titleComponent: AnyComponent?, chatListTitle: NetworkStatusTitle?, leftButton: AnyComponentWithIdentity?, rightButtons: [AnyComponentWithIdentity], backTitle: String?, backPressed: (() -> Void)? ) { self.title = title self.navigationBackTitle = navigationBackTitle self.titleComponent = titleComponent self.chatListTitle = chatListTitle self.leftButton = leftButton self.rightButtons = rightButtons self.backTitle = backTitle self.backPressed = backPressed } public static func ==(lhs: Content, rhs: Content) -> Bool { if lhs.title != rhs.title { return false } if lhs.navigationBackTitle != rhs.navigationBackTitle { return false } if lhs.titleComponent != rhs.titleComponent { return false } if lhs.chatListTitle != rhs.chatListTitle { return false } if lhs.leftButton != rhs.leftButton { return false } if lhs.rightButtons != rhs.rightButtons { return false } if lhs.backTitle != rhs.backTitle { return false } return true } } public let sideInset: CGFloat public let primaryContent: Content? public let secondaryContent: Content? public let secondaryTransition: CGFloat public let networkStatus: HeaderNetworkStatusComponent.Content? public let storySubscriptions: EngineStorySubscriptions? public let storiesFraction: CGFloat public let uploadProgress: Float? public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings public let openStatusSetup: (UIView) -> Void public let toggleIsLocked: () -> Void public init( sideInset: CGFloat, primaryContent: Content?, secondaryContent: Content?, secondaryTransition: CGFloat, networkStatus: HeaderNetworkStatusComponent.Content?, storySubscriptions: EngineStorySubscriptions?, storiesFraction: CGFloat, uploadProgress: Float?, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, openStatusSetup: @escaping (UIView) -> Void, toggleIsLocked: @escaping () -> Void ) { self.sideInset = sideInset self.primaryContent = primaryContent self.secondaryContent = secondaryContent self.secondaryTransition = secondaryTransition self.context = context self.networkStatus = networkStatus self.storySubscriptions = storySubscriptions self.storiesFraction = storiesFraction self.uploadProgress = uploadProgress self.theme = theme self.strings = strings self.openStatusSetup = openStatusSetup self.toggleIsLocked = toggleIsLocked } public static func ==(lhs: ChatListHeaderComponent, rhs: ChatListHeaderComponent) -> Bool { if lhs.sideInset != rhs.sideInset { return false } if lhs.primaryContent != rhs.primaryContent { return false } if lhs.secondaryContent != rhs.secondaryContent { return false } if lhs.secondaryTransition != rhs.secondaryTransition { return false } if lhs.networkStatus != rhs.networkStatus { return false } if lhs.storySubscriptions != rhs.storySubscriptions { return false } if lhs.storiesFraction != rhs.storiesFraction { return false } if lhs.uploadProgress != rhs.uploadProgress { return false } if lhs.context !== rhs.context { return false } if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } return true } private final class BackButtonView: HighlightableButton { private let onPressed: () -> Void let arrowView: UIImageView let titleOffsetContainer: UIView let titleView: ImmediateTextView private var currentColor: UIColor? init(onPressed: @escaping () -> Void) { 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 } if highlighted { self.alpha = 0.6 } else { self.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) } } self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func pressed() { self.onPressed() } func update(title: String, theme: PresentationTheme, availableSize: CGSize, transition: Transition) -> CGSize { self.titleView.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) let titleSize = self.titleView.updateLayout(CGSize(width: 100.0, height: 44.0)) self.accessibilityLabel = title 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) } let iconSpacing: CGFloat = 8.0 let iconOffset: CGFloat = -7.0 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) transition.setPosition(view: self.arrowView, position: arrowFrame.center) transition.setBounds(view: self.arrowView, bounds: CGRect(origin: CGPoint(), size: arrowFrame.size)) transition.setFrame(view: self.titleView, frame: CGRect(origin: CGPoint(x: iconOffset - 3.0 + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize)) return CGSize(width: iconOffset + arrowSize.width + iconSpacing + titleSize.width, height: availableSize.height) } } private final class ContentView: UIView { let backPressed: () -> Void let openStatusSetup: (UIView) -> Void let toggleIsLocked: () -> Void let leftButtonOffsetContainer: UIView var leftButtonViews: [AnyHashable: ComponentView] = [:] let rightButtonOffsetContainer: UIView var rightButtonViews: [AnyHashable: ComponentView] = [:] var backButtonView: BackButtonView? let titleOffsetContainer: UIView let titleScaleContainer: UIView let titleTextView: ImmediateTextView var titleContentView: ComponentView? var chatListTitleView: ChatListTitleView? var contentOffsetFraction: CGFloat = 0.0 private(set) var centerContentWidth: CGFloat = 0.0 private(set) var centerContentOffsetX: CGFloat = 0.0 private(set) var centerContentOrigin: CGFloat = 0.0 init( backPressed: @escaping () -> Void, openStatusSetup: @escaping (UIView) -> Void, toggleIsLocked: @escaping () -> Void ) { self.backPressed = backPressed self.openStatusSetup = openStatusSetup self.toggleIsLocked = toggleIsLocked self.leftButtonOffsetContainer = UIView() self.rightButtonOffsetContainer = UIView() self.titleOffsetContainer = UIView() self.titleScaleContainer = UIView() self.titleTextView = ImmediateTextView() super.init(frame: CGRect()) self.addSubview(self.titleOffsetContainer) self.titleOffsetContainer.addSubview(self.titleScaleContainer) self.addSubview(self.leftButtonOffsetContainer) self.addSubview(self.rightButtonOffsetContainer) self.titleScaleContainer.addSubview(self.titleTextView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let backButtonView = self.backButtonView { if let result = backButtonView.hitTest(self.convert(point, to: backButtonView), with: event) { return result } } for (_, buttonView) in self.leftButtonViews { if let view = buttonView.view, let result = view.hitTest(self.convert(point, to: view), with: event) { return result } } for (_, buttonView) in self.rightButtonViews { if let view = buttonView.view, let result = view.hitTest(self.convert(point, to: view), with: event) { return result } } if let view = self.titleContentView?.view, let result = view.hitTest(self.convert(point, to: view), with: event) { return result } if let view = self.chatListTitleView, let result = view.hitTest(self.convert(point, to: view), with: event) { return result } return nil } func updateContentOffsetFraction(contentOffsetFraction: CGFloat, transition: Transition) { if self.contentOffsetFraction == contentOffsetFraction { return } self.contentOffsetFraction = contentOffsetFraction let translation = 44.0 * contentOffsetFraction * 0.5 var transform = CATransform3DIdentity transform = CATransform3DTranslate(transform, translation, 0.0, 0.0) transition.setSublayerTransform(view: self.titleOffsetContainer, transform: transform) } func updateNavigationTransitionAsPrevious(nextView: ContentView, fraction: CGFloat, transition: Transition, 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)) 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 completion() }) } 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)) } } func updateNavigationTransitionAsNext(previousView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) { 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 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)) } } } func updateNavigationTransitionAsPreviousInplace(nextView: ContentView, fraction: CGFloat, transition: Transition, 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 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 }) } 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: Transition, completion: @escaping () -> Void) { 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)) } } func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, sideContentWidth: CGFloat, sideContentFraction: CGFloat, size: CGSize, transition: Transition) { 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)) transition.setPosition(view: self.titleScaleContainer, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) transition.setBounds(view: self.titleScaleContainer, bounds: CGRect(origin: self.titleScaleContainer.bounds.origin, size: size)) self.titleTextView.attributedText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) let buttonSpacing: CGFloat = 8.0 var leftOffset = sideInset if let backTitle = backTitle { var backButtonTransition = transition let backButtonView: BackButtonView if let current = self.backButtonView { backButtonView = current } else { backButtonTransition = .immediate backButtonView = BackButtonView(onPressed: { [weak self] in guard let self else { return } self.backPressed() }) self.backButtonView = backButtonView self.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 } else if let backButtonView = self.backButtonView { self.backButtonView = nil backButtonView.removeFromSuperview() } var validLeftButtons = Set() if let leftButton = content.leftButton { validLeftButtons.insert(leftButton.id) var buttonTransition = transition var animateButtonIn = false let buttonView: ComponentView if let current = self.leftButtonViews[leftButton.id] { buttonView = current } else { buttonTransition = .immediate animateButtonIn = true buttonView = ComponentView() self.leftButtonViews[leftButton.id] = buttonView } let buttonSize = buttonView.update( transition: buttonTransition, component: leftButton.component, environment: { NavigationButtonComponentEnvironment(theme: theme) }, 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) if let buttonComponentView = buttonView.view { if buttonComponentView.superview == nil { self.leftButtonOffsetContainer.addSubview(buttonComponentView) } buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame) if animateButtonIn { transition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) } } leftOffset = buttonFrame.maxX + buttonSpacing } 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 buttonComponentView?.removeFromSuperview() }) } removeLeftButtons.append(id) } } for id in removeLeftButtons { self.leftButtonViews.removeValue(forKey: id) } var rightOffset = size.width - sideInset var validRightButtons = Set() for rightButton in content.rightButtons { validRightButtons.insert(rightButton.id) var buttonTransition = transition var animateButtonIn = false let buttonView: ComponentView if let current = self.rightButtonViews[rightButton.id] { buttonView = current } else { buttonTransition = .immediate animateButtonIn = true buttonView = ComponentView() self.rightButtonViews[rightButton.id] = buttonView } let buttonSize = buttonView.update( transition: buttonTransition, component: rightButton.component, environment: { NavigationButtonComponentEnvironment(theme: theme) }, 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) if let buttonComponentView = buttonView.view { if buttonComponentView.superview == nil { self.rightButtonOffsetContainer.addSubview(buttonComponentView) } buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame) if animateButtonIn { transition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) } } rightOffset = buttonFrame.minX - buttonSpacing } 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 buttonComponentView?.removeFromSuperview() }) } removeRightButtons.append(id) } } for id in removeRightButtons { self.rightButtonViews.removeValue(forKey: id) } let commonInset: CGFloat = max(leftOffset, size.width - rightOffset) let remainingWidth = size.width - commonInset * 2.0 let titleTextSize = self.titleTextView.updateLayout(CGSize(width: remainingWidth, height: size.height)) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleTextSize.width) / 2.0) + sideContentWidth, y: floor((size.height - titleTextSize.height) / 2.0)), size: titleTextSize) transition.setFrame(view: self.titleTextView, frame: titleFrame) if let titleComponent = content.titleComponent { var titleContentTransition = transition let titleContentView: ComponentView if let current = self.titleContentView { titleContentView = current } else { titleContentTransition = .immediate titleContentView = ComponentView() self.titleContentView = titleContentView } let titleContentSize = titleContentView.update( transition: titleContentTransition, component: titleComponent, environment: {}, containerSize: CGSize(width: remainingWidth, height: size.height) ) if let titleContentComponentView = titleContentView.view { if titleContentComponentView.superview == nil { self.titleScaleContainer.addSubview(titleContentComponentView) } titleContentTransition.setFrame(view: titleContentComponentView, frame: CGRect(origin: CGPoint(x: floor((size.width - titleContentSize.width) / 2.0), y: floor((size.height - titleContentSize.height) / 2.0)), size: titleContentSize)) } } else { if let titleContentView = self.titleContentView { self.titleContentView = nil titleContentView.view?.removeFromSuperview() } } var centerContentWidth: CGFloat = 0.0 var centerContentOffsetX: CGFloat = 0.0 var centerContentOrigin: CGFloat = 0.0 if let chatListTitle = content.chatListTitle { var chatListTitleTransition = transition let chatListTitleView: ChatListTitleView if let current = self.chatListTitleView { chatListTitleView = current } else { chatListTitleTransition = .immediate chatListTitleView = ChatListTitleView(context: context, theme: theme, strings: strings, animationCache: context.animationCache, animationRenderer: context.animationRenderer) chatListTitleView.manualLayout = true self.chatListTitleView = chatListTitleView self.titleScaleContainer.addSubview(chatListTitleView) } let chatListTitleContentSize = size chatListTitleView.theme = theme chatListTitleView.strings = strings chatListTitleView.setTitle(chatListTitle, animated: false) 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) chatListTitleView.openStatusSetup = { [weak self] sourceView in guard let self else { return } self.openStatusSetup(sourceView) } chatListTitleView.toggleIsLocked = { [weak self] in guard let self else { return } self.toggleIsLocked() } let chatListTitleOffset: CGFloat if chatListTitle.activity { chatListTitleOffset = 0.0 } else { chatListTitleOffset = (centerOffset + centerContentOffsetX) * sideContentFraction } centerContentOrigin = chatListTitleOffset + size.width * 0.5 - centerContentWidth * 0.5 chatListTitleTransition.setFrame(view: chatListTitleView, frame: CGRect(origin: CGPoint(x: chatListTitleOffset + floor((size.width - chatListTitleContentSize.width) / 2.0), y: floor((size.height - chatListTitleContentSize.height) / 2.0)), size: chatListTitleContentSize)) } else { if let chatListTitleView = self.chatListTitleView { self.chatListTitleView = nil chatListTitleView.removeFromSuperview() } } self.titleTextView.isHidden = self.chatListTitleView != nil || self.titleContentView != nil self.centerContentWidth = centerContentWidth self.centerContentOffsetX = centerContentOffsetX self.centerContentOrigin = centerContentOrigin } } public final class View: UIView, NavigationBarHeaderView { private var component: ChatListHeaderComponent? private weak var state: EmptyComponentState? private var primaryContentView: ContentView? private var secondaryContentView: ContentView? private var storyOffsetFraction: CGFloat = 0.0 private let storyPeerListExternalState = StoryPeerListComponent.ExternalState() private var storyPeerList: ComponentView? public var storyPeerAction: ((EnginePeer?) -> Void)? public var storyContextPeerAction: ((ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void)? private var effectiveContentView: ContentView? { return self.secondaryContentView ?? self.primaryContentView } override init(frame: CGRect) { super.init(frame: frame) self.storyOffsetFraction = 1.0 } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public var backArrowView: UIView? { 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 } public var titleContentView: UIView? { return self.effectiveContentView?.titleContentView?.view } public func makeTransitionBackArrowView(accentColor: UIColor) -> UIView? { if let backArrowView = self.backArrowView { let view = UIImageView() view.image = NavigationBar.backArrowImage(color: accentColor) view.frame = backArrowView.convert(backArrowView.bounds, to: self) return view } else { return nil } } 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 } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let storyPeerListView = self.storyPeerList?.view { if let result = storyPeerListView.hitTest(self.convert(point, to: storyPeerListView), with: event) { return result } } for subview in self.subviews.reversed() { if !subview.isUserInteractionEnabled || subview.alpha < 0.01 || subview.isHidden { continue } if subview === self.storyPeerList?.view { continue } if let result = subview.hitTest(self.convert(point, to: subview), with: event) { return result } } let defaultResult = super.hitTest(point, with: event) if let defaultResult, defaultResult !== self { return defaultResult } return defaultResult } private func updateContentStoryOffsets(transition: Transition) { } func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.state = state let previousComponent = self.component self.component = component var storyListTransition = transition if let storySubscriptions = component.storySubscriptions { var storyOffsetFraction: CGFloat = 1.0 storyOffsetFraction = component.storiesFraction let _ = storyOffsetFraction //self.storyOffsetFraction = storyOffsetFraction let storyPeerList: ComponentView if let current = self.storyPeerList { storyPeerList = current } else { storyListTransition = .immediate storyPeerList = ComponentView() self.storyPeerList = storyPeerList } let _ = storyPeerList.update( transition: storyListTransition, component: AnyComponent(StoryPeerListComponent( externalState: self.storyPeerListExternalState, context: component.context, theme: component.theme, strings: component.strings, storySubscriptions: storySubscriptions, collapseFraction: 1.0 - component.storiesFraction, uploadProgress: component.uploadProgress, peerAction: { [weak self] peer in guard let self else { return } self.storyPeerAction?(peer) }, contextPeerAction: { [weak self] sourceNode, gesture, peer in guard let self else { return } self.storyContextPeerAction?(sourceNode, gesture, peer) } )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 94.0) ) } if let primaryContent = component.primaryContent { var primaryContentTransition = transition let primaryContentView: ContentView if let current = self.primaryContentView { primaryContentView = current } else { primaryContentTransition = .immediate primaryContentView = ContentView( backPressed: { [weak self] in guard let self, let component = self.component else { return } component.primaryContent?.backPressed?() }, openStatusSetup: { [weak self] sourceView in guard let self else { return } self.component?.openStatusSetup(sourceView) }, toggleIsLocked: { [weak self] in guard let self else { return } self.component?.toggleIsLocked() } ) self.primaryContentView = primaryContentView self.addSubview(primaryContentView) } var sideContentWidth: CGFloat = 0.0 if let storySubscriptions = component.storySubscriptions, !storySubscriptions.items.isEmpty { sideContentWidth = self.storyPeerListExternalState.collapsedWidth + 12.0 } if let chatListTitle = primaryContent.chatListTitle { if chatListTitle.activity { sideContentWidth = 0.0 } } 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) 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() } if let secondaryContent = component.secondaryContent { var secondaryContentTransition = transition let secondaryContentView: ContentView if let current = self.secondaryContentView { secondaryContentView = current } else { secondaryContentTransition = .immediate secondaryContentView = ContentView( backPressed: { [weak self] in guard let self, let component = self.component else { return } component.secondaryContent?.backPressed?() }, openStatusSetup: { [weak self] sourceView in guard let self else { return } self.component?.openStatusSetup(sourceView) }, toggleIsLocked: { [weak self] in guard let self else { return } self.component?.toggleIsLocked() } ) self.secondaryContentView = secondaryContentView self.addSubview(secondaryContentView) } 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) secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) secondaryContentView.updateContentOffsetFraction(contentOffsetFraction: 1.0 - self.storyOffsetFraction, transition: secondaryContentTransition) if let primaryContentView = self.primaryContentView { if let previousComponent = previousComponent, previousComponent.secondaryContent == nil { if self.storyOffsetFraction < 0.8 { 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: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {}) } } if self.storyOffsetFraction < 0.8 { 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: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) } } } else if let secondaryContentView = self.secondaryContentView { self.secondaryContentView = nil 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?.removeFromSuperview() }) } else { primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in secondaryContentView?.removeFromSuperview() }) } } else { secondaryContentView.removeFromSuperview() } } if let storyPeerList = self.storyPeerList, let storyPeerListComponentView = storyPeerList.view { if storyPeerListComponentView.superview == nil { self.addSubview(storyPeerListComponentView) } let storyPeerListMinOffset: CGFloat = -8.0 let storyPeerListMaxOffset: CGFloat = availableSize.height + 2.0 let storyPeerListPosition: CGFloat = storyPeerListMinOffset * (1.0 - component.storiesFraction) + storyPeerListMaxOffset * component.storiesFraction var defaultStoryListX: CGFloat = 0.0 if let primaryContentView = self.primaryContentView { defaultStoryListX = primaryContentView.centerContentOrigin - (self.storyPeerListExternalState.collapsedWidth * 0.5 + 12.0) - availableSize.width * 0.5 } storyListTransition.setFrame(view: storyPeerListComponentView, frame: CGRect(origin: CGPoint(x: -1.0 * availableSize.width * component.secondaryTransition + (1.0 - component.storiesFraction) * defaultStoryListX, y: storyPeerListPosition), size: CGSize(width: availableSize.width, height: 79.0))) var storyListNormalAlpha: CGFloat = 1.0 if let chatListTitle = component.primaryContent?.chatListTitle { if chatListTitle.activity { storyListNormalAlpha = component.storiesFraction } } let storyListAlpha: CGFloat = (1.0 - component.secondaryTransition) * storyListNormalAlpha storyListTransition.setAlpha(view: storyPeerListComponentView, alpha: storyListAlpha) } return availableSize } public func findTitleView() -> ChatListTitleView? { return self.primaryContentView?.chatListTitleView } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } public final class NavigationButtonComponentEnvironment: Equatable { public let theme: PresentationTheme public init(theme: PresentationTheme) { self.theme = theme } public static func ==(lhs: NavigationButtonComponentEnvironment, rhs: NavigationButtonComponentEnvironment) -> Bool { if lhs.theme != rhs.theme { return false } return true } } public final class NavigationButtonComponent: Component { public typealias EnvironmentType = NavigationButtonComponentEnvironment public enum Content: Equatable { case text(title: String, isBold: Bool) case more case icon(imageName: String) case proxy(status: ChatTitleProxyStatus) } public let content: Content public let pressed: (UIView) -> Void public let contextAction: ((UIView, ContextGesture?) -> Void)? public init( content: Content, pressed: @escaping (UIView) -> Void, contextAction: ((UIView, ContextGesture?) -> Void)? = nil ) { self.content = content self.pressed = pressed self.contextAction = contextAction } public static func ==(lhs: NavigationButtonComponent, rhs: NavigationButtonComponent) -> Bool { if lhs.content != rhs.content { return false } return true } public final class View: HighlightTrackingButton { private var textView: ImmediateTextView? private var iconView: UIImageView? private var iconImageName: String? private var proxyNode: ChatTitleProxyNode? private var moreButton: MoreHeaderButton? private var component: NavigationButtonComponent? private var theme: PresentationTheme? override init(frame: CGRect) { super.init(frame: frame) self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) self.highligthedChanged = { [weak self] highlighted in guard let self else { return } if highlighted { self.textView?.alpha = 0.6 self.proxyNode?.alpha = 0.6 self.iconView?.alpha = 0.6 } else { self.textView?.alpha = 1.0 self.textView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) self.proxyNode?.alpha = 1.0 self.proxyNode?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) self.iconView?.alpha = 1.0 self.iconView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) } } } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func pressed() { self.component?.pressed(self) } func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component let theme = environment[NavigationButtonComponentEnvironment.self].value.theme var themeUpdated = false if self.theme !== theme { self.theme = theme themeUpdated = true } let iconOffset: CGFloat = 4.0 var textString: NSAttributedString? var imageName: String? var proxyStatus: ChatTitleProxyStatus? var isMore: Bool = false switch component.content { case let .text(title, isBold): textString = NSAttributedString(string: title, font: isBold ? Font.bold(17.0) : Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) case .more: isMore = true case let .icon(imageNameValue): imageName = imageNameValue case let .proxy(status): proxyStatus = status } var size = CGSize(width: 0.0, height: availableSize.height) if let textString = textString { let textView: ImmediateTextView if let current = self.textView { textView = current } else { textView = ImmediateTextView() textView.isUserInteractionEnabled = false self.textView = textView self.addSubview(textView) } textView.attributedText = textString let textSize = textView.updateLayout(availableSize) size.width = textSize.width textView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((availableSize.height - textSize.height) / 2.0)), size: textSize) } else if let textView = self.textView { self.textView = nil textView.removeFromSuperview() } if let imageName = imageName { let iconView: UIImageView if let current = self.iconView { iconView = current } else { iconView = UIImageView() iconView.isUserInteractionEnabled = false self.iconView = iconView self.addSubview(iconView) } if self.iconImageName != imageName || themeUpdated { self.iconImageName = imageName iconView.image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.rootController.navigationBar.accentTextColor) } if let iconSize = iconView.image?.size { size.width = iconSize.width iconView.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize) } } else if let iconView = self.iconView { self.iconView = nil iconView.removeFromSuperview() self.iconImageName = nil } if let proxyStatus = proxyStatus { let proxyNode: ChatTitleProxyNode if let current = self.proxyNode { proxyNode = current } else { proxyNode = ChatTitleProxyNode(theme: theme) proxyNode.isUserInteractionEnabled = false self.proxyNode = proxyNode self.addSubnode(proxyNode) } let proxySize = CGSize(width: 30.0, height: 30.0) size.width = proxySize.width proxyNode.theme = theme proxyNode.status = proxyStatus proxyNode.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - proxySize.height) / 2.0)), size: proxySize) } else if let proxyNode = self.proxyNode { self.proxyNode = nil proxyNode.removeFromSupernode() } if isMore { let moreButton: MoreHeaderButton if let current = self.moreButton, !themeUpdated { moreButton = current } else { if let moreButton = self.moreButton { moreButton.removeFromSupernode() self.moreButton = nil } moreButton = MoreHeaderButton(color: theme.rootController.navigationBar.buttonColor) moreButton.isUserInteractionEnabled = true moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) moreButton.onPressed = { [weak self] in guard let self, let component = self.component else { return } self.moreButton?.play() component.pressed(self) } moreButton.contextAction = { [weak self] sourceNode, gesture in guard let self, let component = self.component else { return } self.moreButton?.play() component.contextAction?(self, gesture) } self.moreButton = moreButton self.addSubnode(moreButton) } let buttonSize = CGSize(width: 26.0, height: 44.0) size.width = buttonSize.width moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) moreButton.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - buttonSize.height) / 2.0)), size: buttonSize) } else if let moreButton = self.moreButton { self.moreButton = nil moreButton.removeFromSupernode() } return size } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }