import Foundation import UIKit import Display import ComponentFlow import TelegramPresentationData import AccountContext import ChatListTitleView import AppBundle import StoryPeerListComponent import TelegramCore import MoreHeaderButton import GlassBackgroundComponent 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: ComponentTransition) -> 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: ComponentTransition) -> 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 backPressed: (() -> Void)? public init( title: String, navigationBackTitle: String?, titleComponent: AnyComponent?, chatListTitle: NetworkStatusTitle?, leftButton: AnyComponentWithIdentity?, rightButtons: [AnyComponentWithIdentity], backPressed: (() -> Void)? ) { self.title = title self.navigationBackTitle = navigationBackTitle self.titleComponent = titleComponent self.chatListTitle = chatListTitle self.leftButton = leftButton self.rightButtons = rightButtons 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.backPressed == nil) != (rhs.backPressed == nil) { 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 storiesIncludeHidden: Bool public let storiesFraction: CGFloat public let storiesUnlocked: Bool public let uploadProgress: [EnginePeer.Id: 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?, storiesIncludeHidden: Bool, storiesFraction: CGFloat, storiesUnlocked: Bool, uploadProgress: [EnginePeer.Id: 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.storiesIncludeHidden = storiesIncludeHidden self.storiesFraction = storiesFraction self.storiesUnlocked = storiesUnlocked 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.storiesIncludeHidden != rhs.storiesIncludeHidden { return false } if lhs.storiesFraction != rhs.storiesFraction { return false } if lhs.storiesUnlocked != rhs.storiesUnlocked { 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 private var currentColor: UIColor? init(onPressed: @escaping () -> Void) { self.onPressed = onPressed self.arrowView = UIImageView() super.init(frame: CGRect()) self.addSubview(self.arrowView) 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(theme: PresentationTheme, strings: PresentationStrings, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.accessibilityLabel = strings.Common_Back self.accessibilityTraits = [.button] 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 size = CGSize(width: 44.0, height: availableSize.height) let arrowSize = self.arrowView.image?.size ?? CGSize(width: 13.0, height: 22.0) 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)) return size } } private final class ContentView: UIView { let backPressed: () -> Void let openStatusSetup: (UIView) -> Void let toggleIsLocked: () -> Void let leftButtonsContainer: UIView var leftButtonViews: [AnyHashable: ComponentView] = [:] let rightButtonsContainer: 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 centerContentLeftInset: CGFloat = 0.0 private(set) var centerContentRightInset: CGFloat = 0.0 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, openStatusSetup: @escaping (UIView) -> Void, toggleIsLocked: @escaping () -> Void ) { self.backPressed = backPressed self.openStatusSetup = openStatusSetup self.toggleIsLocked = toggleIsLocked self.leftButtonsContainer = UIView() self.rightButtonsContainer = UIView() self.titleOffsetContainer = UIView() self.titleScaleContainer = UIView() self.titleTextView = ImmediateTextView() super.init(frame: CGRect()) self.addSubview(self.titleOffsetContainer) self.titleOffsetContainer.addSubview(self.titleScaleContainer) 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: ComponentTransition) { 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, 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 completion() }) } 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() }) 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) { 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() }) 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() }) 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, 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)) 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)) let titleText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) let titleTextUpdated = self.titleTextView.attributedText != titleText self.titleTextView.attributedText = titleText let buttonSpacing: CGFloat = 0.0 var nextLeftButtonX: CGFloat = 0.0 if displayBackButton { 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.leftButtonsContainer.addSubview(backButtonView) } 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() } var validLeftButtons = Set() if let leftButton = content.leftButton { validLeftButtons.insert(leftButton.id) if nextLeftButtonX != 0.0 { nextLeftButtonX += buttonSpacing } 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: nextLeftButtonX, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) if let buttonComponentView = buttonView.view { if buttonComponentView.superview == nil { self.leftButtonsContainer.addSubview(buttonComponentView) } buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame) if animateButtonIn { alphaTransition.animateBlur(layer: buttonComponentView.layer, fromRadius: 10.0, toRadius: 0.0) alphaTransition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) } } nextLeftButtonX += buttonSize.width } var removeLeftButtons: [AnyHashable] = [] for (id, buttonView) in self.leftButtonViews { if !validLeftButtons.contains(id) { if let buttonComponentView = buttonView.view { alphaTransition.setBlur(layer: buttonComponentView.layer, radius: 10.0) alphaTransition.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 nextRightButtonX: CGFloat = 0.0 var validRightButtons = Set() for rightButton in content.rightButtons.reversed() { validRightButtons.insert(rightButton.id) if nextRightButtonX != 0.0 { nextRightButtonX += buttonSpacing } 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: nextRightButtonX, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) if let buttonComponentView = buttonView.view { if buttonComponentView.superview == nil { self.rightButtonsContainer.addSubview(buttonComponentView) } buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame) if animateButtonIn { alphaTransition.animateBlur(layer: buttonComponentView.layer, fromRadius: 10.0, toRadius: 0.0) alphaTransition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) } } nextRightButtonX += buttonSize.width } var removeRightButtons: [AnyHashable] = [] for (id, buttonView) in self.rightButtonViews { if !validRightButtons.contains(id) { if let buttonComponentView = buttonView.view { alphaTransition.setBlur(layer: buttonComponentView.layer, radius: 10.0) alphaTransition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in buttonComponentView?.removeFromSuperview() }) } removeRightButtons.append(id) } } for id in removeRightButtons { self.rightButtonViews.removeValue(forKey: id) } 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)) let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleTextSize.width) / 2.0) + sideContentWidth, y: floor((size.height - titleTextSize.height) / 2.0)), size: titleTextSize) if titleTextUpdated { self.titleTextView.frame = titleFrame } else { 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 centerContentLeftInset: CGFloat = 0.0 centerContentLeftInset = nextLeftButtonX + 4.0 var centerContentRightInset: CGFloat = 0.0 centerContentRightInset = nextRightButtonX + 20.0 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) let centerOffset = sideContentWidth * 0.5 centerContentOffsetX = -max(0.0, centerOffset + titleContentRect.maxX - 2.0 - (size.width - sideInset - nextRightButtonX)) 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 self.centerContentRightInset = centerContentRightInset self.centerContentLeftInset = centerContentLeftInset } } 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 leftButtonsContainer: UIView private let rightButtonsContainer: UIView private var leftButtonsBackgroundContainer: GlassBackgroundView? private var rightButtonsBackgroundContainer: GlassBackgroundView? private let storyPeerListExternalState = StoryPeerListComponent.ExternalState() private var storyPeerList: ComponentView? public var storyPeerAction: ((EnginePeer?) -> Void)? public var storyContextPeerAction: ((ContextExtractedContentContainingNode, ContextGesture, EnginePeer) -> Void)? public var storyComposeAction: ((CGFloat) -> Void)? private var effectiveContentView: ContentView? { return self.secondaryContentView ?? self.primaryContentView } 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 } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public var backArrowView: UIView? { return self.effectiveContentView?.backButtonView?.arrowView } public var rightButtonView: UIView? { return self.effectiveContentView?.rightButtonViews.first?.value.view } public var rightButtonViews: [AnyHashable: UIView] { return self.effectiveContentView?.rightButtonViews.reduce(into: [:], { result, view in result[view.key] = view.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 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: ComponentTransition) { } func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.state = state let previousComponent = self.component self.component = component var primaryContentTransition = transition if var primaryContent = component.primaryContent { 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) self.leftButtonsContainer.addSubview(primaryContentView.leftButtonsContainer) self.rightButtonsContainer.addSubview(primaryContentView.rightButtonsContainer) } let sideContentWidth: CGFloat = 0.0 if component.storySubscriptions != nil { primaryContent = Content( title: "", navigationBackTitle: primaryContent.navigationBackTitle, titleComponent: nil, chatListTitle: nil, leftButton: primaryContent.leftButton, rightButtons: primaryContent.rightButtons, backPressed: primaryContent.backPressed ) } 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 if let storySubscriptions = component.storySubscriptions { let storyPeerList: ComponentView if let current = self.storyPeerList { storyPeerList = current } else { storyListTransition = .immediate storyPeerList = ComponentView() self.storyPeerList = storyPeerList } var primaryTitle = "" var primaryTitleHasLock = false var primaryTitleHasActivity = false var primaryTitlePeerStatus: StoryPeerListComponent.PeerStatus? if let primaryContent = component.primaryContent { if let chatListTitle = primaryContent.chatListTitle { primaryTitle = chatListTitle.text primaryTitleHasLock = chatListTitle.isPasscodeSet primaryTitleHasActivity = chatListTitle.activity if let peerStatus = chatListTitle.peerStatus { switch peerStatus { case .premium: primaryTitlePeerStatus = .premium case let .emoji(status): primaryTitlePeerStatus = .emoji(status) } } } else { primaryTitle = primaryContent.title } } let _ = storyPeerList.update( transition: storyListTransition, component: AnyComponent(StoryPeerListComponent( externalState: self.storyPeerListExternalState, context: component.context, theme: component.theme, strings: component.strings, sideInset: component.sideInset, title: primaryTitle, titleHasLock: primaryTitleHasLock, titleHasActivity: primaryTitleHasActivity, titlePeerStatus: primaryTitlePeerStatus, minTitleX: self.primaryContentView?.centerContentLeftInset ?? 0.0, maxTitleX: availableSize.width - (self.primaryContentView?.centerContentRightInset ?? 0.0), useHiddenList: component.storiesIncludeHidden, storySubscriptions: storySubscriptions, collapseFraction: 1.0 - component.storiesFraction, unlocked: component.storiesUnlocked, 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) }, openStatusSetup: { [weak self] sourceView in guard let self else { return } self.component?.openStatusSetup(sourceView) }, lockAction: { [weak self] in guard let self else { return } self.component?.toggleIsLocked() }, composeAction: { [weak self] offset in guard let self else { return } self.storyComposeAction?(offset) } )), environment: {}, containerSize: CGSize(width: availableSize.width, height: ChatListNavigationBar.storiesScrollHeight) ) } var secondaryContentTransition = transition var secondaryContentIsAnimatingIn = false var removedSecondaryContentView: ContentView? if let secondaryContent = component.secondaryContent { 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 { 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 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, 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) 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, width: availableSize.width, fraction: 0.0, transition: .immediate, completion: {}) secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, storyPeerListView: self.storyPeerListView(), 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, 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, 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() } } if let storyPeerList = self.storyPeerList, let storyPeerListComponentView = storyPeerList.view as? StoryPeerListComponent.View { if storyPeerListComponentView.superview == nil { self.addSubview(storyPeerListComponentView) } let storyPeerListMaxOffset: CGFloat = availableSize.height + 2.0 var storiesX: CGFloat = 0.0 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))) let storyListNormalAlpha: CGFloat = 1.0 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 } public func findTitleView() -> ChatListTitleView? { return self.primaryContentView?.chatListTitleView } public func emojiStatus() -> PeerEmojiStatus? { guard let component = self.component else { return nil } if let _ = component.storySubscriptions, let primaryContent = component.primaryContent, let chatListTitle = primaryContent.chatListTitle, let peerStatus = chatListTitle.peerStatus, case let .emoji(emojiStatus) = peerStatus { return emojiStatus } else if let peerStatus = self.findTitleView()?.title.peerStatus, case let .emoji(emojiStatus) = peerStatus { return emojiStatus } return nil } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } }