Files
Swiftgram/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift
2025-11-22 03:17:19 +08:00

1185 lines
62 KiB
Swift

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<Empty>, 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<Empty>, 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<Empty>?
public let chatListTitle: NetworkStatusTitle?
public let leftButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>?
public let rightButtons: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>]
public let backPressed: (() -> Void)?
public init(
title: String,
navigationBackTitle: String?,
titleComponent: AnyComponent<Empty>?,
chatListTitle: NetworkStatusTitle?,
leftButton: AnyComponentWithIdentity<NavigationButtonComponentEnvironment>?,
rightButtons: [AnyComponentWithIdentity<NavigationButtonComponentEnvironment>],
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<NavigationButtonComponentEnvironment>] = [:]
let rightButtonsContainer: UIView
var rightButtonViews: [AnyHashable: ComponentView<NavigationButtonComponentEnvironment>] = [:]
var backButtonView: BackButtonView?
let titleOffsetContainer: UIView
let titleScaleContainer: UIView
let titleTextView: ImmediateTextView
var titleContentView: ComponentView<Empty>?
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<AnyHashable>()
if let leftButton = content.leftButton {
validLeftButtons.insert(leftButton.id)
if nextLeftButtonX != 0.0 {
nextLeftButtonX += buttonSpacing
}
var buttonTransition = transition
var animateButtonIn = false
let buttonView: ComponentView<NavigationButtonComponentEnvironment>
if let current = self.leftButtonViews[leftButton.id] {
buttonView = current
} else {
buttonTransition = .immediate
animateButtonIn = true
buttonView = ComponentView<NavigationButtonComponentEnvironment>()
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<AnyHashable>()
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<NavigationButtonComponentEnvironment>
if let current = self.rightButtonViews[rightButton.id] {
buttonView = current
} else {
buttonTransition = .immediate
animateButtonIn = true
buttonView = ComponentView<NavigationButtonComponentEnvironment>()
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<Empty>
if let current = self.titleContentView {
titleContentView = current
} else {
titleContentTransition = .immediate
titleContentView = ComponentView<Empty>()
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<Empty>?
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<Empty>, 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<Empty>
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<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}