import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import MergeLists
import ActivityIndicator
import AccountContext
import SearchBarNode
import SearchUI
import ContextUI
import AnimationCache
import MultiAnimationRenderer
import TelegramUIPreferences
import ActionPanelComponent
import ComponentDisplayAdapters
import ComponentFlow
import ChatFolderLinkPreviewScreen
import ChatListHeaderComponent
import StoryPeerListComponent

public enum ChatListContainerNodeFilter: Equatable {
    case all
    case filter(ChatListFilter)
    
    public var id: ChatListFilterTabEntryId {
        switch self {
        case .all:
            return .all
        case let .filter(filter):
            return .filter(filter.id)
        }
    }
    
    public var filter: ChatListFilter? {
        switch self {
        case .all:
            return nil
        case let .filter(filter):
            return filter
        }
    }
}

public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
    private let context: AccountContext
    private weak var controller: ChatListControllerImpl?
    let location: ChatListControllerLocation
    private let chatListMode: ChatListNodeMode
    private let previewing: Bool
    private let isInlineMode: Bool
    private let controlsHistoryPreload: Bool
    private let filterBecameEmpty: (ChatListFilter?) -> Void
    private let filterEmptyAction: (ChatListFilter?) -> Void
    private let secondaryEmptyAction: () -> Void
    private let openArchiveSettings: () -> Void
    
    fileprivate var onStoriesLockedUpdated: ((Bool) -> Void)?
    
    fileprivate var onFilterSwitch: (() -> Void)?
    
    private var presentationData: PresentationData
    
    private let animationCache: AnimationCache
    private let animationRenderer: MultiAnimationRenderer
    
    private var itemNodes: [ChatListFilterTabEntryId: ChatListContainerItemNode] = [:]
    private var pendingItemNode: (ChatListFilterTabEntryId, ChatListContainerItemNode, Disposable)?
    private(set) var availableFilters: [ChatListContainerNodeFilter] = [.all] {
        didSet {
            self.availableFiltersPromise.set(self.availableFilters)
        }
    }
    private let availableFiltersPromise = ValuePromise<[ChatListContainerNodeFilter]>([.all], ignoreRepeated: true)
    var availableFiltersSignal: Signal<[ChatListContainerNodeFilter], NoError> {
        return self.availableFiltersPromise.get()
    }
    
    private var filtersLimit: Int32? = nil
    private var selectedId: ChatListFilterTabEntryId
    
    var hintUpdatedStoryExpansion: Bool = false
    var ignoreStoryUnlockedScrolling: Bool = false
    var tempTopInset: CGFloat = 0.0 {
        didSet {
            for (_, itemNode) in self.itemNodes {
                itemNode.listNode.tempTopInset = self.tempTopInset
            }
            if let pendingItemNode = self.pendingItemNode {
                pendingItemNode.1.listNode.tempTopInset = self.tempTopInset
            }
        }
    }
    
    var initialScrollingOffset: CGFloat?
    
    public private(set) var transitionFraction: CGFloat = 0.0
    private var transitionFractionOffset: CGFloat = 0.0
    private var disableItemNodeOperationsWhileAnimating: Bool = false
    private var validLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, insets: UIEdgeInsets, isReorderingFilters: Bool, isEditing: Bool, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat)?
    
    private var scrollingOffset: (navigationHeight: CGFloat, offset: CGFloat)?
    
    private var enableAdjacentFilterLoading: Bool = false
    
    private var panRecognizer: InteractiveTransitionGestureRecognizer?
    
    let leftSeparatorLayer: SimpleLayer
    
    private let _ready = Promise<Bool>()
    public var ready: Signal<Bool, NoError> {
        return _ready.get()
    }
    
    private let _validLayoutReady = Promise<Bool>()
    var validLayoutReady: Signal<Bool, NoError> {
        return _validLayoutReady.get()
    }
    
    private var currentItemNodeValue: ChatListContainerItemNode?
    public var currentItemNode: ChatListNode {
        return self.currentItemNodeValue!.listNode
    }
    
    private let currentItemStateValue = Promise<(state: ChatListNodeState, filterId: Int32?)>()
    var currentItemState: Signal<(state: ChatListNodeState, filterId: Int32?), NoError> {
        return self.currentItemStateValue.get()
    }
    
    public var currentItemFilterUpdated: ((ChatListFilterTabEntryId, CGFloat, ContainedViewLayoutTransition, Bool) -> Void)?
    public var currentItemFilter: ChatListFilterTabEntryId {
        return self.currentItemNode.chatListFilter.flatMap { .filter($0.id) } ?? .all
    }
    
    private var didSetupContentOffset = false
    private var isSettingUpContentOffset = false
    
    private func applyItemNodeAsCurrent(id: ChatListFilterTabEntryId, itemNode: ChatListContainerItemNode) {
        if let previousItemNode = self.currentItemNodeValue {
            previousItemNode.listNode.activateSearch = nil
            previousItemNode.listNode.presentAlert = nil
            previousItemNode.listNode.present = nil
            previousItemNode.listNode.push = nil
            previousItemNode.listNode.toggleArchivedFolderHiddenByDefault = nil
            previousItemNode.listNode.hidePsa = nil
            previousItemNode.listNode.deletePeerChat = nil
            previousItemNode.listNode.deletePeerThread = nil
            previousItemNode.listNode.setPeerThreadStopped = nil
            previousItemNode.listNode.setPeerThreadPinned = nil
            previousItemNode.listNode.setPeerThreadHidden = nil
            previousItemNode.listNode.peerSelected = nil
            previousItemNode.listNode.groupSelected = nil
            previousItemNode.listNode.updatePeerGrouping = nil
            previousItemNode.listNode.contentOffsetChanged = nil
            previousItemNode.listNode.contentScrollingEnded = nil
            previousItemNode.listNode.didBeginInteractiveDragging = nil
            previousItemNode.listNode.endedInteractiveDragging = { _ in }
            previousItemNode.listNode.shouldStopScrolling = nil
            previousItemNode.listNode.activateChatPreview = nil
            previousItemNode.listNode.openStories = nil
            previousItemNode.listNode.addedVisibleChatsWithPeerIds = nil
            previousItemNode.listNode.didBeginSelectingChats = nil
            previousItemNode.listNode.canExpandHiddenItems = nil
            
            previousItemNode.accessibilityElementsHidden = true
        }
        self.currentItemNodeValue = itemNode
        itemNode.accessibilityElementsHidden = false
        
        itemNode.listNode.activateSearch = { [weak self] in
            self?.activateSearch?()
        }
        itemNode.listNode.presentAlert = { [weak self] text in
            self?.presentAlert?(text)
        }
        itemNode.listNode.present = { [weak self] c in
            self?.present?(c)
        }
        itemNode.listNode.push = { [weak self] c in
            self?.push?(c)
        }
        itemNode.listNode.toggleArchivedFolderHiddenByDefault = { [weak self] in
            self?.toggleArchivedFolderHiddenByDefault?()
        }
        itemNode.listNode.hidePsa = { [weak self] peerId in
            self?.hidePsa?(peerId)
        }
        itemNode.listNode.deletePeerChat = { [weak self] peerId, joined in
            self?.deletePeerChat?(peerId, joined)
        }
        itemNode.listNode.deletePeerThread = { [weak self] peerId, threadId in
            self?.deletePeerThread?(peerId, threadId)
        }
        itemNode.listNode.setPeerThreadStopped = { [weak self] peerId, threadId, isStopped in
            self?.setPeerThreadStopped?(peerId, threadId, isStopped)
        }
        itemNode.listNode.setPeerThreadPinned = { [weak self] peerId, threadId, isPinned in
            self?.setPeerThreadPinned?(peerId, threadId, isPinned)
        }
        itemNode.listNode.setPeerThreadHidden = { [weak self] peerId, threadId, isHidden in
            self?.setPeerThreadHidden?(peerId, threadId, isHidden)
        }
        itemNode.listNode.peerSelected = { [weak self] peerId, threadId, animated, activateInput, promoInfo in
            self?.peerSelected?(peerId, threadId, animated, activateInput, promoInfo)
        }
        itemNode.listNode.groupSelected = { [weak self] groupId in
            self?.groupSelected?(groupId)
        }
        itemNode.listNode.updatePeerGrouping = { [weak self] peerId, group in
            self?.updatePeerGrouping?(peerId, group)
        }
        itemNode.listNode.contentOffsetChanged = { [weak self, weak itemNode] offset in
            guard let self, let itemNode else {
                return
            }
            if self.isSettingUpContentOffset {
                return
            }
            
            if !self.didSetupContentOffset, let initialScrollingOffset = self.initialScrollingOffset {
                self.initialScrollingOffset = nil
                self.didSetupContentOffset = true
                self.isSettingUpContentOffset = true
                
                let _ = itemNode.listNode.scrollToOffsetFromTop(initialScrollingOffset, animated: false)
                
                let offset = itemNode.listNode.visibleContentOffset()
                self.contentOffset = offset
                self.contentOffsetChanged?(offset, self.currentItemNode)
                
                self.isSettingUpContentOffset = false
                return
            }
            
            if !self.isInlineMode, itemNode.listNode.isTracking && !self.currentItemNode.startedScrollingAtUpperBound && self.tempTopInset == 0.0 {
                if case let .known(value) = offset {
                    if value < -1.0 {
                        if let controller = self.controller, let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) {
                            self.currentItemNode.startedScrollingAtUpperBound = true
                            self.tempTopInset = ChatListNavigationBar.storiesScrollHeight
                        }
                    }
                }
            }
            
            self.contentOffset = offset
            self.contentOffsetChanged?(offset, self.currentItemNode)
            
            if !self.isInlineMode, self.currentItemNode.startedScrollingAtUpperBound && self.tempTopInset != 0.0 {
                if case let .known(value) = offset {
                    if value > 4.0 {
                        self.currentItemNode.startedScrollingAtUpperBound = false
                        self.tempTopInset = 0.0
                    } else if value <= -ChatListNavigationBar.storiesScrollHeight {
                    } else if value > -82.0 {
                    }
                } else if case .unknown = offset {
                    self.currentItemNode.startedScrollingAtUpperBound = false
                    self.tempTopInset = 0.0
                }
            }
        }
        itemNode.listNode.didBeginInteractiveDragging = { [weak self] listView in
            guard let self else {
                return
            }
            
            self.didBeginInteractiveDragging?(listView)
            
            if self.isInlineMode {
                return
            }
            
            guard let validLayout = self.validLayout else {
                return
            }
            
            let tempTopInset: CGFloat
            if validLayout.inlineNavigationLocation != nil {
                tempTopInset = 0.0
            } else if self.currentItemNode.startedScrollingAtUpperBound && !self.isInlineMode {
                if let controller = self.controller, let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) {
                    tempTopInset = ChatListNavigationBar.storiesScrollHeight
                } else {
                    tempTopInset = 0.0
                }
            } else {
                tempTopInset = 0.0
            }
            if self.tempTopInset != tempTopInset {
                self.tempTopInset = tempTopInset
                self.hintUpdatedStoryExpansion = true
                self.currentItemNode.contentOffsetChanged?(self.currentItemNode.visibleContentOffset())
                self.hintUpdatedStoryExpansion = false
            }
        }
        itemNode.listNode.endedInteractiveDragging = { [weak self] _ in
            guard let self else {
                return
            }
            self.endedInteractiveDragging?(self.currentItemNode)
        }
        itemNode.listNode.shouldStopScrolling = { [weak self] velocity in
            guard let self else {
                return false
            }
            return self.shouldStopScrolling?(self.currentItemNode, velocity) ?? false
        }
        itemNode.listNode.contentScrollingEnded = { [weak self] listView in
            guard let self else {
                return false
            }
            
            return self.contentScrollingEnded?(listView) ?? false
            //DispatchQueue.main.async { [weak self] in
            //    let _ = self?.contentScrollingEnded?(listView)
            //}
            
            //return false
        }
        itemNode.listNode.activateChatPreview = { [weak self] item, threadId, sourceNode, gesture, location in
            self?.activateChatPreview?(item, threadId, sourceNode, gesture, location)
        }
        itemNode.listNode.openStories = { [weak self] subject, itemNode in
            self?.openStories?(subject, itemNode)
        }
        itemNode.listNode.addedVisibleChatsWithPeerIds = { [weak self] ids in
            self?.addedVisibleChatsWithPeerIds?(ids)
        }
        itemNode.listNode.didBeginSelectingChats = { [weak self] in
            self?.didBeginSelectingChats?()
        }
        itemNode.listNode.canExpandHiddenItems = { [weak self] in
            guard let self, let canExpandHiddenItems = self.canExpandHiddenItems else {
                return false
            }
            return canExpandHiddenItems()
        }
        
        self.currentItemStateValue.set(itemNode.listNode.state |> map { state in
            let filterId: Int32?
            switch id {
            case .all:
                filterId = nil
            case let .filter(filter):
                filterId = filter
            }
            return (state, filterId)
        })
        
        let enablePreload = context.sharedContext.accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]))
        |> map { sharedData -> Bool in
            var automaticMediaDownloadSettings: MediaAutoDownloadSettings
            if let value = sharedData.entries[ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings]?.get(MediaAutoDownloadSettings.self) {
                automaticMediaDownloadSettings = value
            } else {
                automaticMediaDownloadSettings = MediaAutoDownloadSettings.defaultSettings
            }
            return automaticMediaDownloadSettings.energyUsageSettings.autodownloadInBackground
        }
        |> distinctUntilChanged
        
        if self.controlsHistoryPreload, case .chatList(groupId: .root) = self.location {
            self.context.account.viewTracker.chatListPreloadItems.set(combineLatest(queue: .mainQueue(),
                context.sharedContext.enablePreloads.get(),
                itemNode.listNode.preloadItems.get(),
                enablePreload
            )
            |> map { enablePreloads, preloadItems, enablePreload -> Set<ChatHistoryPreloadItem> in
                if !enablePreloads || !enablePreload {
                    return Set()
                } else {
                    return Set(preloadItems)
                }
            })
        }
    }
    
    public var activateSearch: (() -> Void)?
    var presentAlert: ((String) -> Void)?
    var present: ((ViewController) -> Void)?
    var push: ((ViewController) -> Void)?
    var toggleArchivedFolderHiddenByDefault: (() -> Void)?
    var hidePsa: ((EnginePeer.Id) -> Void)?
    var deletePeerChat: ((EnginePeer.Id, Bool) -> Void)?
    var deletePeerThread: ((EnginePeer.Id, Int64) -> Void)?
    var setPeerThreadStopped: ((EnginePeer.Id, Int64, Bool) -> Void)?
    var setPeerThreadPinned: ((EnginePeer.Id, Int64, Bool) -> Void)?
    var setPeerThreadHidden: ((EnginePeer.Id, Int64, Bool) -> Void)?
    public var peerSelected: ((EnginePeer, Int64?, Bool, Bool, ChatListNodeEntryPromoInfo?) -> Void)?
    var groupSelected: ((EngineChatList.Group) -> Void)?
    var updatePeerGrouping: ((EnginePeer.Id, Bool) -> Void)?
    var contentOffset: ListViewVisibleContentOffset?
    public var contentOffsetChanged: ((ListViewVisibleContentOffset, ListView) -> Void)?
    public var contentScrollingEnded: ((ListView) -> Bool)?
    var didBeginInteractiveDragging: ((ListView) -> Void)?
    var endedInteractiveDragging: ((ListView) -> Void)?
    var shouldStopScrolling: ((ListView, CGFloat) -> Bool)?
    var activateChatPreview: ((ChatListItem, Int64?, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
    var openStories: ((ChatListNode.OpenStoriesSubject, ASDisplayNode?) -> Void)?
    var addedVisibleChatsWithPeerIds: (([EnginePeer.Id]) -> Void)?
    var didBeginSelectingChats: (() -> Void)?
    var canExpandHiddenItems: (() -> Bool)?
    public var displayFilterLimit: (() -> Void)?
    
    public init(context: AccountContext, controller: ChatListControllerImpl?, location: ChatListControllerLocation, chatListMode: ChatListNodeMode = .chatList(appendContacts: true), previewing: Bool, controlsHistoryPreload: Bool, isInlineMode: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, filterBecameEmpty: @escaping (ChatListFilter?) -> Void, filterEmptyAction: @escaping (ChatListFilter?) -> Void, secondaryEmptyAction: @escaping () -> Void, openArchiveSettings: @escaping () -> Void) {
        self.context = context
        self.controller = controller
        self.location = location
        self.chatListMode = chatListMode
        self.previewing = previewing
        self.isInlineMode = isInlineMode
        self.filterBecameEmpty = filterBecameEmpty
        self.filterEmptyAction = filterEmptyAction
        self.secondaryEmptyAction = secondaryEmptyAction
        self.openArchiveSettings = openArchiveSettings
        self.controlsHistoryPreload = controlsHistoryPreload
        
        self.presentationData = presentationData
        self.animationCache = animationCache
        self.animationRenderer = animationRenderer
        
        self.selectedId = .all
        
        self.leftSeparatorLayer = SimpleLayer()
        self.leftSeparatorLayer.isHidden = true
        self.leftSeparatorLayer.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor.cgColor
        
        super.init()
        
        self.backgroundColor = presentationData.theme.chatList.backgroundColor
        
        let itemNode = ChatListContainerItemNode(context: self.context, controller: self.controller, location: self.location, filter: nil, chatListMode: chatListMode, previewing: self.previewing, isInlineMode: self.isInlineMode, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, becameEmpty: { [weak self] filter in
            self?.filterBecameEmpty(filter)
        }, emptyAction: { [weak self] filter in
            self?.filterEmptyAction(filter)
        }, secondaryEmptyAction: { [weak self] in
            self?.secondaryEmptyAction()
        }, openArchiveSettings: { [weak self] in
            self?.openArchiveSettings()
        }, autoSetReady: true, isMainTab: nil)
        self.itemNodes[.all] = itemNode
        self.addSubnode(itemNode)
        
        self._ready.set(itemNode.listNode.ready)
        
        self.applyItemNodeAsCurrent(id: .all, itemNode: itemNode)
        
        let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
            guard let self, self.availableFilters.count > 1 || (self.controller?.isStoryPostingAvailable == true && !(self.context.sharedContext.callManager?.hasActiveCall ?? false)) else {
                return []
            }
            guard case .chatList(.root) = self.location else {
                return []
            }
            switch self.currentItemNode.visibleContentOffset() {
            case let .known(value):
                if value < -self.currentItemNode.tempTopInset {
                    return []
                }
            case .none, .unknown:
                break
            }
            if !self.currentItemNode.isNavigationInAFinalState {
                return []
            }
            if self.availableFilters.count > 1 {
                return [.leftCenter, .rightCenter]
            } else {
                return [.rightEdge]
            }
        }, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0))
        panRecognizer.delegate = self
        panRecognizer.delaysTouchesBegan = false
        panRecognizer.cancelsTouchesInView = true
        self.panRecognizer = panRecognizer
        self.view.addGestureRecognizer(panRecognizer)
        
        self.view.layer.addSublayer(self.leftSeparatorLayer)
    }
    
    deinit {
        self.pendingItemNode?.2.dispose()
    }
    
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return false
    }
    
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
            return false
        }
        if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
            return true
        }
        return false
    }
    
    @objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
        let filtersLimit = self.filtersLimit.flatMap({ $0 + 1 }) ?? Int32(self.availableFilters.count)
        let maxFilterIndex = min(Int(filtersLimit), self.availableFilters.count) - 1
        
        switch recognizer.state {
        case .began:
            self.onFilterSwitch?()
            
            self.transitionFractionOffset = 0.0
            if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout, let itemNode = self.itemNodes[self.selectedId] {
                for (id, itemNode) in self.itemNodes {
                    if id != selectedId {
                        itemNode.emptyNode?.restartAnimation()
                        
                        if let controller = self.controller, let chatListDisplayNode = controller.displayNode as? ChatListControllerNode, let navigationBarComponentView = chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View, let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
                            let scrollOffset = clippedScrollOffset
                            
                            let _ = itemNode.listNode.scrollToOffsetFromTop(scrollOffset, animated: false)
                        }
                    }
                }
                
                if let presentationLayer = itemNode.layer.presentation() {
                    self.transitionFraction = presentationLayer.frame.minX / layout.size.width
                    self.transitionFractionOffset = self.transitionFraction
                    if !self.transitionFraction.isZero {
                        for (_, itemNode) in self.itemNodes {
                            itemNode.layer.removeAllAnimations()
                        }
                        self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
                        self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, .immediate, true)
                    }
                }
            }
        case .changed:
            if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
                let translation = recognizer.translation(in: self.view)
                var transitionFraction = translation.x / layout.size.width
                
                var transition: ContainedViewLayoutTransition = .immediate
                
                func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat {
                    let bandedOffset = offset - bandingStart
                    let range: CGFloat = 600.0
                    let coefficient: CGFloat = 0.4
                    return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range
                }
                     
                if case .compact = layout.metrics.widthClass, self.controller?.isStoryPostingAvailable == true && !(self.context.sharedContext.callManager?.hasActiveCall ?? false) {
                    let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false
                    if selectedIndex <= 0 && translation.x > 0.0 {
                        transitionFraction = 0.0
                        self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width)
                    } else if translation.x <= 0.0 && cameraIsAlreadyOpened {
                        self.controller?.storyCameraPanGestureChanged(transitionFraction: 0.0)
                    }
                    
                    if cameraIsAlreadyOpened {
                        transitionFraction = 0.0
                        return
                    }
                } else {
                    if selectedIndex <= 0 && translation.x > 0.0 {
                        let overscroll = translation.x
                        transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
                    }
                }
                
                if selectedIndex >= maxFilterIndex && translation.x < 0.0 {
                    let overscroll = -translation.x
                    transitionFraction = -rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
                    
                    if let filtersLimit = self.filtersLimit, selectedIndex >= filtersLimit - 1 {
                        transitionFraction = 0.0
                        self.transitionFractionOffset = 0.0
                        recognizer.isEnabled = false
                        recognizer.isEnabled = true
                        
                        transition = .animated(duration: 0.45, curve: .spring)
                        self.displayFilterLimit?()
                    }
                }
                self.transitionFraction = transitionFraction + self.transitionFractionOffset
                if let currentItemNode = self.currentItemNodeValue {
                    let isNavigationHidden = currentItemNode.listNode.isNavigationHidden
                    for (_, itemNode) in self.itemNodes {
                        if itemNode !== currentItemNode {
                            itemNode.listNode.adjustScrollOffsetForNavigation(isNavigationHidden: isNavigationHidden)
                        }
                    }
                }
                self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
                self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false)
            }
        case .cancelled, .ended:
            if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout, let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
                let translation = recognizer.translation(in: self.view)
                let velocity = recognizer.velocity(in: self.view)
                var directionIsToRight: Bool?
                if abs(velocity.x) > 10.0 {
                    if translation.x < 0.0 {
                        if velocity.x >= 0.0 {
                            directionIsToRight = nil
                        } else {
                            directionIsToRight = true
                        }
                    } else {
                        if velocity.x <= 0.0 {
                            directionIsToRight = nil
                        } else {
                            directionIsToRight = false
                        }
                    }
                } else {
                    if abs(translation.x) > layout.size.width / 2.0 {
                        directionIsToRight = translation.x > layout.size.width / 2.0
                    }
                }
                
                let hasStoryCameraTransition = self.controller?.hasStoryCameraTransition ?? false
                if hasStoryCameraTransition {
                    self.controller?.storyCameraPanGestureEnded(transitionFraction: translation.x / layout.size.width, velocity: velocity.x)
                }
                var applyNodeAsCurrent: ChatListFilterTabEntryId?
                
                if let directionIsToRight = directionIsToRight {
                    var updatedIndex = selectedIndex
                    if directionIsToRight {
                        updatedIndex = min(updatedIndex + 1, maxFilterIndex)
                    } else {
                        updatedIndex = max(updatedIndex - 1, 0)
                    }
                    let switchToId = self.availableFilters[updatedIndex].id
                    if switchToId != self.selectedId, let itemNode = self.itemNodes[switchToId] {
                        let _ = itemNode
                        self.selectedId = switchToId
                        applyNodeAsCurrent = switchToId
                    }
                }
                self.transitionFraction = 0.0
                let transition: ContainedViewLayoutTransition = .animated(duration: 0.45, curve: .spring)
                self.disableItemNodeOperationsWhileAnimating = true
                self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: transition)
                DispatchQueue.main.async {
                    self.disableItemNodeOperationsWhileAnimating = false
                    if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
                        self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
                    }
                }
                                    
                if let switchToId = applyNodeAsCurrent, let itemNode = self.itemNodes[switchToId] {
                    self.applyItemNodeAsCurrent(id: switchToId, itemNode: itemNode)
                }
                self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false)
            }
        default:
            break
        }
    }
    
    func fixContentOffset(offset: CGFloat) {
        self.currentItemNode.fixContentOffset(offset: offset)
    }
    
    public func updatePresentationData(_ presentationData: PresentationData) {
        self.presentationData = presentationData
        
        if let validLayout = self.validLayout {
            if let _ = validLayout.inlineNavigationLocation {
                self.backgroundColor = self.presentationData.theme.chatList.backgroundColor.mixedWith(self.presentationData.theme.chatList.pinnedItemBackgroundColor, alpha: validLayout.inlineNavigationTransitionFraction)
            } else {
                self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
            }
        }
        
        self.leftSeparatorLayer.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor.cgColor
        
        for (_, itemNode) in self.itemNodes {
            itemNode.updatePresentationData(presentationData)
        }
    }
    
    func playArchiveAnimation() {
        if let itemNode = self.itemNodes[self.selectedId] {
            itemNode.listNode.forEachVisibleItemNode { node in
                if let node = node as? ChatListItemNode {
                    node.playArchiveAnimation()
                }
            }
        }
    }
    
    public func scrollToTop(animated: Bool, adjustForTempInset: Bool) {
        if let itemNode = self.itemNodes[self.selectedId] {
            itemNode.listNode.scrollToPosition(.top(adjustForTempInset: adjustForTempInset), animated: animated)
        }
    }
    
    func updateSelectedChatLocation(data: ChatLocation?, progress: CGFloat, transition: ContainedViewLayoutTransition) {
        for (_, itemNode) in self.itemNodes {
            itemNode.listNode.updateSelectedChatLocation(data, progress: progress, transition: transition)
        }
    }
    
    func updateState(onlyCurrent: Bool = true, _ f: (ChatListNodeState) -> ChatListNodeState) {
        self.currentItemNode.updateState(f)
        let updatedState = self.currentItemNode.currentState
        for (id, itemNode) in self.itemNodes {
            if id != self.selectedId {
                if onlyCurrent {
                    itemNode.listNode.updateState { state in
                        var state = state
                        state.editing = updatedState.editing
                        state.selectedPeerIds = updatedState.selectedPeerIds
                        return state
                    }
                } else {
                    itemNode.listNode.updateState(f)
                }
            }
        }
    }
    
    public func updateAvailableFilters(_ availableFilters: [ChatListContainerNodeFilter], limit: Int32?) {
        if self.availableFilters != availableFilters {
            let apply: () -> Void = { [weak self] in
                guard let strongSelf = self else {
                    return
                }
                strongSelf.availableFilters = availableFilters
                strongSelf.filtersLimit = limit
                if let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = strongSelf.validLayout {
                    strongSelf.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
                }
            }
            if !availableFilters.contains(where: { $0.id == self.selectedId }) {
                self.switchToFilter(id: .all, completion: {
                    apply()
                })
            } else {
                apply()
            }
        }
    }
    
    public func updateEnableAdjacentFilterLoading(_ value: Bool) {
        if value != self.enableAdjacentFilterLoading {
            self.enableAdjacentFilterLoading = value
            
            if self.enableAdjacentFilterLoading, let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
                self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
            }
        }
    }
    
    public func switchToFilter(id: ChatListFilterTabEntryId, animated: Bool = true, completion: (() -> Void)? = nil) {
        self.onFilterSwitch?()
        if id != self.selectedId, let index = self.availableFilters.firstIndex(where: { $0.id == id }) {
            if let itemNode = self.itemNodes[id] {
                guard let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout else {
                    return
                }
                
                if let controller = self.controller, let chatListDisplayNode = controller.displayNode as? ChatListControllerNode, let navigationBarComponentView = chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View, let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
                    let scrollOffset = clippedScrollOffset
                    
                    let _ = itemNode.listNode.scrollToOffsetFromTop(scrollOffset, animated: false)
                }
                
                self.selectedId = id
                self.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
                let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring)
                self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: transition)
                self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, transition, false)
                itemNode.emptyNode?.restartAnimation()
                completion?()
            } else if self.pendingItemNode == nil {
                let itemNode = ChatListContainerItemNode(context: self.context, controller: self.controller, location: self.location, filter: self.availableFilters[index].filter, chatListMode: self.chatListMode, previewing: self.previewing, isInlineMode: self.isInlineMode, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, becameEmpty: { [weak self] filter in
                    self?.filterBecameEmpty(filter)
                }, emptyAction: { [weak self] filter in
                    self?.filterEmptyAction(filter)
                }, secondaryEmptyAction: { [weak self] in
                    self?.secondaryEmptyAction()
                }, openArchiveSettings: { [weak self] in
                    self?.openArchiveSettings()
                }, autoSetReady: !animated, isMainTab: index == 0)
                self.pendingItemNode?.2.dispose()
                let disposable = MetaDisposable()
                self.pendingItemNode = (id, itemNode, disposable)
                
                if !animated {
                    self.selectedId = id
                    self.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
                    self.currentItemFilterUpdated?(self.currentItemFilter, self.transitionFraction, .immediate, false)
                }
                
                disposable.set((itemNode.listNode.ready
                |> take(1)
                |> deliverOnMainQueue).startStrict(next: { [weak self, weak itemNode] _ in
                    guard let strongSelf = self, let itemNode = itemNode, itemNode === strongSelf.pendingItemNode?.1 else {
                        return
                    }
                    
                    strongSelf.pendingItemNode?.2.dispose()
                    strongSelf.pendingItemNode = nil
                    itemNode.listNode.tempTopInset = strongSelf.tempTopInset
                    
                    if let controller = strongSelf.controller, let chatListDisplayNode = controller.displayNode as? ChatListControllerNode, let navigationBarComponentView = chatListDisplayNode.navigationBarView.view as? ChatListNavigationBar.View, let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
                        let scrollOffset = clippedScrollOffset
                        
                        let _ = itemNode.listNode.scrollToOffsetFromTop(scrollOffset, animated: false)
                    }
                    
                    guard let (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = strongSelf.validLayout else {
                        strongSelf.itemNodes[id] = itemNode
                        strongSelf.addSubnode(itemNode)
                        
                        strongSelf.selectedId = id
                        strongSelf.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
                        strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, .immediate, false)
                        
                        completion?()
                        return
                    }
                    
                    let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.35, curve: .spring) : .immediate
                    if let previousIndex = strongSelf.availableFilters.firstIndex(where: { $0.id == strongSelf.selectedId }), let index = strongSelf.availableFilters.firstIndex(where: { $0.id == id }) {
                        let previousId = strongSelf.selectedId
                        let offsetDirection: CGFloat = index < previousIndex ? 1.0 : -1.0
                        let offset = offsetDirection * layout.size.width
                        
                        var validNodeIds: [ChatListFilterTabEntryId] = []
                        for i in max(0, index - 1) ... min(strongSelf.availableFilters.count - 1, index + 1) {
                            validNodeIds.append(strongSelf.availableFilters[i].id)
                        }
                        
                        var removeIds: [ChatListFilterTabEntryId] = []
                        for (id, _) in strongSelf.itemNodes {
                            if !validNodeIds.contains(id) {
                                removeIds.append(id)
                            }
                        }
                        for id in removeIds {
                            if let itemNode = strongSelf.itemNodes.removeValue(forKey: id) {
                                if id == previousId {
                                    transition.updateFrame(node: itemNode, frame: itemNode.frame.offsetBy(dx: offset, dy: 0.0), completion: { [weak itemNode] _ in
                                        itemNode?.removeFromSupernode()
                                    })
                                } else {
                                    itemNode.removeFromSupernode()
                                }
                            }
                        }
                        
                        strongSelf.itemNodes[id] = itemNode
                        strongSelf.addSubnode(itemNode)
                        
                        let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: layout.size)
                        itemNode.frame = itemFrame
                        
                        transition.animatePositionAdditive(node: itemNode, offset: CGPoint(x: -offset, y: 0.0))
                                                
                        itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
                        if let scrollingOffset = strongSelf.scrollingOffset {
                            itemNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: .immediate)
                        }
                        
                        strongSelf.selectedId = id
                        if let currentItemNode = strongSelf.currentItemNodeValue {
                            itemNode.listNode.adjustScrollOffsetForNavigation(isNavigationHidden: currentItemNode.listNode.isNavigationHidden)
                        }
                        strongSelf.applyItemNodeAsCurrent(id: id, itemNode: itemNode)
                        
                        strongSelf.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
                        
                        strongSelf.currentItemFilterUpdated?(strongSelf.currentItemFilter, strongSelf.transitionFraction, transition, false)
                    }
                    
                    completion?()
                }))
                
                if let (layout, _, visualNavigationHeight, originalNavigationHeight, _, insets, _, _, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset) = self.validLayout {
                    itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, storiesInset: storiesInset, transition: .immediate)
                    
                    if let scrollingOffset = self.scrollingOffset {
                        itemNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: .immediate)
                    }
                    return
                }
            }
        }
    }
    
    func updateScrollingOffset(navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition) {
        self.scrollingOffset = (navigationHeight, offset)
        for (_, itemNode) in self.itemNodes {
            itemNode.updateScrollingOffset(navigationHeight: navigationHeight, offset: offset, transition: transition)
        }
    }
    
    public func update(layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, originalNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, insets: UIEdgeInsets, isReorderingFilters: Bool, isEditing: Bool, inlineNavigationLocation: ChatListControllerLocation?, inlineNavigationTransitionFraction: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) {
        self.validLayout = (layout, navigationBarHeight, visualNavigationHeight, originalNavigationHeight, cleanNavigationBarHeight, insets, isReorderingFilters, isEditing, inlineNavigationLocation, inlineNavigationTransitionFraction, storiesInset)
        
        self._validLayoutReady.set(.single(true))
        
        transition.updateAlpha(node: self, alpha: isReorderingFilters ? 0.5 : 1.0)
        self.isUserInteractionEnabled = !isReorderingFilters
        
        if let _ = inlineNavigationLocation {
            transition.updateBackgroundColor(node: self, color: self.presentationData.theme.chatList.backgroundColor.mixedWith(self.presentationData.theme.chatList.pinnedItemBackgroundColor, alpha: inlineNavigationTransitionFraction))
        } else {
            transition.updateBackgroundColor(node: self, color: self.presentationData.theme.chatList.backgroundColor)
        }
        
        self.panRecognizer?.isEnabled = !isEditing
        
        transition.updateFrame(layer: self.leftSeparatorLayer, frame: CGRect(origin: CGPoint(x: -UIScreenPixel, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height)))
        
        if let selectedIndex = self.availableFilters.firstIndex(where: { $0.id == self.selectedId }) {
            var validNodeIds: [ChatListFilterTabEntryId] = []
            for i in max(0, selectedIndex - 1) ... min(self.availableFilters.count - 1, selectedIndex + 1) {
                let id = self.availableFilters[i].id
                validNodeIds.append(id)
                
                if self.itemNodes[id] == nil && self.enableAdjacentFilterLoading && !self.disableItemNodeOperationsWhileAnimating {
                    let itemNode = ChatListContainerItemNode(context: self.context, controller: self.controller, location: self.location, filter: self.availableFilters[i].filter, chatListMode: self.chatListMode, previewing: self.previewing, isInlineMode: self.isInlineMode, controlsHistoryPreload: self.controlsHistoryPreload, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, becameEmpty: { [weak self] filter in
                        self?.filterBecameEmpty(filter)
                    }, emptyAction: { [weak self] filter in
                        self?.filterEmptyAction(filter)
                    }, secondaryEmptyAction: { [weak self] in
                        self?.secondaryEmptyAction()
                    }, openArchiveSettings: { [weak self] in
                        self?.openArchiveSettings()
                    }, autoSetReady: false, isMainTab: i == 0)
                    itemNode.listNode.tempTopInset = self.tempTopInset
                    self.itemNodes[id] = itemNode
                }
            }
            
            var removeIds: [ChatListFilterTabEntryId] = []
            var animateSlidingIds: [ChatListFilterTabEntryId] = []
            var slidingOffset: CGFloat?
            for (id, itemNode) in self.itemNodes {
                if !validNodeIds.contains(id) {
                    removeIds.append(id)
                }
                guard let index = self.availableFilters.firstIndex(where: { $0.id == id }) else {
                    continue
                }
                let indexDistance = CGFloat(index - selectedIndex) + self.transitionFraction
                
                let wasAdded = itemNode.supernode == nil
                var nodeTransition = transition
                if wasAdded {
                    self.addSubnode(itemNode)
                    nodeTransition = .immediate
                }
                
                let itemFrame = CGRect(origin: CGPoint(x: indexDistance * layout.size.width, y: 0.0), size: layout.size)
                if !wasAdded && slidingOffset == nil {
                    slidingOffset = itemNode.frame.minX - itemFrame.minX
                }
                nodeTransition.updateFrame(node: itemNode, frame: itemFrame, completion: { _ in
                })
                
                var itemInlineNavigationTransitionFraction = inlineNavigationTransitionFraction
                if indexDistance != 0 {
                    if itemInlineNavigationTransitionFraction != 0.0 || itemInlineNavigationTransitionFraction != 1.0 {
                        itemInlineNavigationTransitionFraction = itemNode.validLayout?.inlineNavigationTransitionFraction ?? 0.0
                    }
                }
                
                itemNode.listNode.isMainTab.set(self.availableFilters.firstIndex(where: { $0.id == id }) == 0 ? true : false)
                itemNode.updateLayout(size: layout.size, insets: insets, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: itemInlineNavigationTransitionFraction, storiesInset: storiesInset, transition: nodeTransition)
                if let scrollingOffset = self.scrollingOffset {
                    itemNode.updateScrollingOffset(navigationHeight: scrollingOffset.navigationHeight, offset: scrollingOffset.offset, transition: nodeTransition)
                }
                
                if wasAdded, case .animated = transition {
                    animateSlidingIds.append(id)
                }
            }
            if let slidingOffset = slidingOffset {
                for id in animateSlidingIds {
                    if let itemNode = self.itemNodes[id] {
                        transition.animatePositionAdditive(node: itemNode, offset: CGPoint(x: slidingOffset, y: 0.0), completion: {
                        })
                    }
                }
            }
            if !self.disableItemNodeOperationsWhileAnimating {
                for id in removeIds {
                    if let itemNode = self.itemNodes.removeValue(forKey: id) {
                        itemNode.removeFromSupernode()
                    }
                }
            }
        }
    }
}

final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
    private let context: AccountContext
    private let location: ChatListControllerLocation
    private var presentationData: PresentationData
    private let animationCache: AnimationCache
    private let animationRenderer: MultiAnimationRenderer
    
    let mainContainerNode: ChatListContainerNode
    
    var effectiveContainerNode: ChatListContainerNode {
        if let inlineStackContainerNode = self.inlineStackContainerNode {
            return inlineStackContainerNode
        } else {
            return self.mainContainerNode
        }
    }
    
    private(set) var inlineStackContainerTransitionFraction: CGFloat = 0.0
    private(set) var inlineStackContainerNode: ChatListContainerNode?
    private var inlineContentPanRecognizer: InteractiveTransitionGestureRecognizer?
    var temporaryContentOffsetChangeTransition: ContainedViewLayoutTransition?
    
    private var tapRecognizer: UITapGestureRecognizer?
    var navigationBar: NavigationBar?
    let navigationBarView = ComponentView<Empty>()
    weak var controller: ChatListControllerImpl?
    
    var toolbar: Toolbar?
    private var toolbarNode: ToolbarNode?
    var toolbarActionSelected: ((ToolbarActionOption) -> Void)?
    
    private var isSearchDisplayControllerActive: Bool = false
    private var skipSearchDisplayControllerLayout: Bool = false
    private(set) var searchDisplayController: SearchDisplayController?
    
    var isReorderingFilters: Bool = false
    var didBeginSelectingChatsWhileEditing: Bool = false
    var isEditing: Bool = false
    
    var tempAllowAvatarExpansion: Bool = false
    private var tempDisableStoriesAnimations: Bool = false
    private var tempNavigationScrollingTransition: ContainedViewLayoutTransition?
    
    private var allowOverscrollStoryExpansion: Bool = false
    private var currentOverscrollStoryExpansionTimestamp: Double?
    
    private var allowOverscrollItemExpansion: Bool = false
    private var currentOverscrollItemExpansionTimestamp: Double?
    
    private var containerLayout: (layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, storiesInset: CGFloat)?
    
    var contentScrollingEnded: ((ListView) -> Bool)?
    
    var requestDeactivateSearch: (() -> Void)?
    var requestOpenPeerFromSearch: ((EnginePeer, Int64?, Bool) -> Void)?
    var requestOpenRecentPeerOptions: ((EnginePeer) -> Void)?
    var requestOpenMessageFromSearch: ((EnginePeer, Int64?, EngineMessage.Id, Bool) -> Void)?
    var requestAddContact: ((String) -> Void)?
    var peerContextAction: ((EnginePeer, ChatListSearchContextActionSource, ASDisplayNode, ContextGesture?, CGPoint?) -> Void)?
    var dismissSelfIfCompletedPresentation: (() -> Void)?
    var isEmptyUpdated: ((Bool) -> Void)?
    var emptyListAction: ((EnginePeer.Id?) -> Void)?
    var cancelEditing: (() -> Void)?

    let debugListView = ListView()
    
    init(context: AccountContext, location: ChatListControllerLocation, previewing: Bool, controlsHistoryPreload: Bool, presentationData: PresentationData, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, controller: ChatListControllerImpl) {
        self.context = context
        self.location = location
        self.presentationData = presentationData
        self.animationCache = animationCache
        self.animationRenderer = animationRenderer
        
        var filterBecameEmpty: ((ChatListFilter?) -> Void)?
        var filterEmptyAction: ((ChatListFilter?) -> Void)?
        var secondaryEmptyAction: (() -> Void)?
        var openArchiveSettings: (() -> Void)?
        self.mainContainerNode = ChatListContainerNode(context: context, controller: controller, location: location, previewing: previewing, controlsHistoryPreload: controlsHistoryPreload, isInlineMode: false, presentationData: presentationData, animationCache: animationCache, animationRenderer: animationRenderer, filterBecameEmpty: { filter in
            filterBecameEmpty?(filter)
        }, filterEmptyAction: { filter in
            filterEmptyAction?(filter)
        }, secondaryEmptyAction: {
            secondaryEmptyAction?()
        }, openArchiveSettings: {
            openArchiveSettings?()
        })
        
        self.controller = controller
        
        super.init()
        
        self.setViewBlock({
            return UITracingLayerView()
        })
        
        self.backgroundColor = presentationData.theme.chatList.backgroundColor
        
        self.addSubnode(self.mainContainerNode)
        
        self.mainContainerNode.contentOffsetChanged = { [weak self] offset, listView in
            self?.contentOffsetChanged(offset: offset, listView: listView, isPrimary: true)
        }
        self.mainContainerNode.contentScrollingEnded = { [weak self] listView in
            return self?.contentScrollingEnded(listView: listView, isPrimary: true) ?? false
        }
        self.mainContainerNode.didBeginInteractiveDragging = { [weak self] listView in
            self?.didBeginInteractiveDragging(listView: listView, isPrimary: true)
        }
        self.mainContainerNode.endedInteractiveDragging = { [weak self] listView in
            self?.endedInteractiveDragging(listView: listView, isPrimary: true)
        }
        self.mainContainerNode.shouldStopScrolling = { [weak self] listView, velocity in
            return self?.shouldStopScrolling(listView: listView, velocity: velocity, isPrimary: true) ?? false
        }
        
        self.addSubnode(self.debugListView)
        
        filterBecameEmpty = { [weak self] _ in
            guard let strongSelf = self else {
                return
            }
            if case .chatList(.archive) = strongSelf.location {
                strongSelf.dismissSelfIfCompletedPresentation?()
            }
        }
        filterEmptyAction = { [weak self] _ in
            guard let strongSelf = self else {
                return
            }
            strongSelf.emptyListAction?(nil)
        }
        
        secondaryEmptyAction = { [weak self] in
            guard let strongSelf = self, case let .forum(peerId) = strongSelf.location, let controller = strongSelf.controller else {
                return
            }
            
            let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false))
            (controller.navigationController as? NavigationController)?.replaceController(controller, with: chatController, animated: false)
        }
        
        openArchiveSettings = { [weak self] in
            guard let self, let controller = self.controller else {
                return
            }
            controller.push(self.context.sharedContext.makeArchiveSettingsController(context: self.context))
        }
        
        self.mainContainerNode.onFilterSwitch = { [weak self] in
            if let strongSelf = self {
                strongSelf.controller?.dismissAllUndoControllers()
            }
        }
        
        self.mainContainerNode.onStoriesLockedUpdated = { [weak self] isLocked in
            guard let self else {
                return
            }
            if isLocked {
                self.controller?.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
                //self.controller?.requestLayout(transition: .immediate)
            } else {
                self.controller?.requestLayout(transition: .immediate)
            }
        }
        
        self.mainContainerNode.canExpandHiddenItems = { [weak self] in
            guard let self, let controller = self.controller else {
                return false
            }
            
            if let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) {
                if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
                    if navigationBarComponentView.storiesUnlocked {
                        return true
                    }
                }
                return false
            } else {
                return true
            }
        }
        
        let inlineContentPanRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.inlineContentPanGesture(_:)), allowedDirections: { [weak self] _ in
            guard let strongSelf = self, strongSelf.inlineStackContainerNode != nil else {
                return []
            }
            let directions: InteractiveTransitionGestureRecognizerDirections = [.rightCenter]
            return directions
        }, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0))
        inlineContentPanRecognizer.delegate = self
        inlineContentPanRecognizer.delaysTouchesBegan = false
        inlineContentPanRecognizer.cancelsTouchesInView = true
        self.inlineContentPanRecognizer = inlineContentPanRecognizer
        self.view.addGestureRecognizer(inlineContentPanRecognizer)
    }
    
    override func didLoad() {
        super.didLoad()
        
        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
        self.tapRecognizer = tapRecognizer
        self.view.addGestureRecognizer(tapRecognizer)
        tapRecognizer.isEnabled = false
    }
    
    @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
        if case .ended = recognizer.state {
            self.cancelEditing?()
        }
    }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return false
    }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
            return false
        }
        if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
            return true
        }
        return false
    }
    
    @objc private func inlineContentPanGesture(_ recognizer: UIPanGestureRecognizer) {
        switch recognizer.state {
        case .began:
            break
        case .changed:
            if let inlineStackContainerNode = self.inlineStackContainerNode {
                let translation = recognizer.translation(in: self.view)
                var transitionFraction = translation.x / inlineStackContainerNode.bounds.width
                transitionFraction = 1.0 - max(0.0, min(1.0, transitionFraction))
                self.inlineStackContainerTransitionFraction = transitionFraction
                self.controller?.requestLayout(transition: .immediate)
            }
        case .cancelled, .ended:
            if let inlineStackContainerNode = self.inlineStackContainerNode {
                let translation = recognizer.translation(in: self.view)
                let velocity = recognizer.velocity(in: self.view)
                var directionIsToRight: Bool?
                if abs(velocity.x) > 10.0 {
                    if translation.x > 0.0 {
                        if velocity.x <= 0.0 {
                            directionIsToRight = nil
                        } else {
                            directionIsToRight = true
                        }
                    } else {
                        if velocity.x >= 0.0 {
                            directionIsToRight = nil
                        } else {
                            directionIsToRight = false
                        }
                    }
                } else {
                    if abs(translation.x) > inlineStackContainerNode.bounds.width / 2.0 {
                        directionIsToRight = translation.x > inlineStackContainerNode.bounds.width / 2.0
                    }
                }
                
                if let directionIsToRight = directionIsToRight, directionIsToRight {
                    self.controller?.setInlineChatList(location: nil)
                } else {
                    self.inlineStackContainerTransitionFraction = 1.0
                    self.controller?.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
                }
            }
        default:
            break
        }
    }
    
    func updatePresentationData(_ presentationData: PresentationData) {
        self.presentationData = presentationData
        
        self.backgroundColor = self.presentationData.theme.chatList.backgroundColor
        
        self.mainContainerNode.updatePresentationData(presentationData)
        self.inlineStackContainerNode?.updatePresentationData(presentationData)
        self.searchDisplayController?.updatePresentationData(presentationData)
        
        if let toolbarNode = self.toolbarNode {
            toolbarNode.updateTheme(ToolbarTheme(rootControllerTheme: self.presentationData.theme))
        }
    }
    
    private func updateNavigationBar(layout: ContainerViewLayout, deferScrollApplication: Bool, transition: Transition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) {
        let headerContent = self.controller?.updateHeaderContent()
        
        var tabsNode: ASDisplayNode?
        var tabsNodeIsSearch = false
        
        if let value = self.controller?.searchTabsNode {
            tabsNode = value
            tabsNodeIsSearch = true
        } else if let value = self.controller?.tabsNode, self.controller?.hasTabs == true {
            tabsNode = value
        }
        
        var effectiveStorySubscriptions: EngineStorySubscriptions?
        if let controller = self.controller, case .forum = controller.location {
            effectiveStorySubscriptions = nil
        } else {
            if let controller = self.controller, let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) {
                effectiveStorySubscriptions = controller.orderedStorySubscriptions
            } else {
                effectiveStorySubscriptions = EngineStorySubscriptions(accountItem: nil, items: [], hasMoreToken: nil)
            }
        }
        
        let navigationBarSize = self.navigationBarView.update(
            transition: transition,
            component: AnyComponent(ChatListNavigationBar(
                context: self.context,
                theme: self.presentationData.theme,
                strings: self.presentationData.strings,
                statusBarHeight: layout.statusBarHeight ?? 0.0,
                sideInset: layout.safeInsets.left,
                isSearchActive: self.isSearchDisplayControllerActive,
                primaryContent: headerContent?.primaryContent,
                secondaryContent: headerContent?.secondaryContent,
                secondaryTransition: self.inlineStackContainerTransitionFraction,
                storySubscriptions: effectiveStorySubscriptions,
                storiesIncludeHidden: self.location == .chatList(groupId: .archive),
                uploadProgress: self.controller?.storyUploadProgress ?? [:],
                tabsNode: tabsNode,
                tabsNodeIsSearch: tabsNodeIsSearch,
                accessoryPanelContainer: self.controller?.accessoryPanelContainer,
                accessoryPanelContainerHeight: self.controller?.accessoryPanelContainerHeight ?? 0.0,
                activateSearch: { [weak self] searchContentNode in
                    guard let self, let controller = self.controller else {
                        return
                    }
                    
                    var isForum = false
                    if case .forum = controller.location {
                        isForum = true
                    }
                    
                    let filter: ChatListSearchFilter = isForum ? .topics : .chats
                    
                    controller.activateSearch(
                        filter: filter,
                        query: nil,
                        skipScrolling: false,
                        searchContentNode: searchContentNode
                    )
                },
                openStatusSetup: { [weak self] sourceView in
                    guard let self, let controller = self.controller else {
                        return
                    }
                    controller.openStatusSetup(sourceView: sourceView)
                },
                allowAutomaticOrder: { [weak self] in
                    guard let self, let controller = self.controller else {
                        return
                    }
                    controller.allowAutomaticOrder()
                }
            )),
            environment: {},
            containerSize: layout.size
        )
        if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
            if deferScrollApplication {
                navigationBarComponentView.deferScrollApplication = true
            }
            
            if navigationBarComponentView.superview == nil {
                self.view.addSubview(navigationBarComponentView)
            }
            transition.setFrame(view: navigationBarComponentView, frame: CGRect(origin: CGPoint(), size: navigationBarSize))
            
            return (navigationBarSize.height, 0.0)
        } else {
            return (0.0, 0.0)
        }
    }
    
    private func updateNavigationScrolling(navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
        var mainOffset: CGFloat
        if let contentOffset = self.mainContainerNode.contentOffset, case let .known(value) = contentOffset {
            mainOffset = value
        } else {
            mainOffset = navigationHeight
        }
        
        self.mainContainerNode.updateScrollingOffset(navigationHeight: navigationHeight, offset: mainOffset, transition: transition)
        
        mainOffset = min(mainOffset, ChatListNavigationBar.searchScrollHeight)
        if abs(mainOffset) < 0.1 {
            mainOffset = 0.0
        }
        
        let resultingOffset: CGFloat
        if let inlineStackContainerNode = self.inlineStackContainerNode {
            var inlineOffset: CGFloat
            if let contentOffset = inlineStackContainerNode.contentOffset, case let .known(value) = contentOffset {
                inlineOffset = value
            } else {
                inlineOffset = navigationHeight
            }
            inlineOffset = min(inlineOffset, ChatListNavigationBar.searchScrollHeight)
            if abs(inlineOffset) < 0.1 {
                inlineOffset = 0.0
            }
            
            resultingOffset = mainOffset * (1.0 - self.inlineStackContainerTransitionFraction) + inlineOffset * self.inlineStackContainerTransitionFraction
        } else {
            resultingOffset = mainOffset
        }
        
        var offset = resultingOffset
        if self.isSearchDisplayControllerActive {
            offset = 0.0
        }
        
        var allowAvatarsExpansion: Bool = true
        if !self.mainContainerNode.currentItemNode.startedScrollingAtUpperBound && !self.tempAllowAvatarExpansion {
            if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
                if !navigationBarComponentView.storiesUnlocked {
                    allowAvatarsExpansion = false
                }
            }
        }
        
        if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
            navigationBarComponentView.applyScroll(offset: offset, allowAvatarsExpansion: allowAvatarsExpansion, forceUpdate: false, transition: Transition(transition).withUserData(ChatListNavigationBar.AnimationHint(
                disableStoriesAnimations: self.tempDisableStoriesAnimations,
                crossfadeStoryPeers: false
            )))
        }
        
        let mainDelta: CGFloat
        if let _ = self.inlineStackContainerNode {
            mainDelta = resultingOffset - max(0.0, mainOffset)
        } else {
            mainDelta = 0.0
        }
        transition.updateSublayerTransformOffset(layer: self.mainContainerNode.layer, offset: CGPoint(x: 0.0, y: -mainDelta))
    }
    
    func requestNavigationBarLayout(transition: Transition) {
        guard let (layout, _, _, _, _) = self.containerLayout else {
            return
        }
        let _ = self.updateNavigationBar(layout: layout, deferScrollApplication: false, transition: transition)
    }
    
    func scrollToStories(animated: Bool) {
        if self.inlineStackContainerNode != nil {
            return
        }
        
        if let controller = self.controller, let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) {
            let _ = storySubscriptions
        
            self.tempAllowAvatarExpansion = true
            self.tempDisableStoriesAnimations = !animated
            self.tempNavigationScrollingTransition = animated ? .animated(duration: 0.3, curve: .custom(0.33, 0.52, 0.25, 0.99)) : .immediate
            self.mainContainerNode.scrollToTop(animated: animated, adjustForTempInset: true)
            self.tempAllowAvatarExpansion = false
            self.tempDisableStoriesAnimations = false
            tempNavigationScrollingTransition = nil
        }
    }
    
    func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, visualNavigationHeight: CGFloat, cleanNavigationBarHeight: CGFloat, storiesInset: CGFloat, transition: ContainedViewLayoutTransition) {
        var navigationBarHeight = navigationBarHeight
        var visualNavigationHeight = visualNavigationHeight
        var cleanNavigationBarHeight = cleanNavigationBarHeight
        var storiesInset = storiesInset
        
        let navigationBarLayout = self.updateNavigationBar(layout: layout, deferScrollApplication: true, transition: Transition(transition))
        self.mainContainerNode.initialScrollingOffset = ChatListNavigationBar.searchScrollHeight + navigationBarLayout.storiesInset
        
        navigationBarHeight = navigationBarLayout.navigationHeight
        visualNavigationHeight = navigationBarLayout.navigationHeight
        cleanNavigationBarHeight = navigationBarLayout.navigationHeight
        storiesInset = navigationBarLayout.storiesInset
        
        self.containerLayout = (layout, navigationBarHeight, visualNavigationHeight, cleanNavigationBarHeight, storiesInset)
        
        var insets = layout.insets(options: [.input])
        insets.top += navigationBarHeight
        insets.left += layout.safeInsets.left
        insets.right += layout.safeInsets.right
        
        if let toolbar = self.toolbar {
            var tabBarHeight: CGFloat
            var options: ContainerViewLayoutInsetOptions = []
            if layout.metrics.widthClass == .regular {
                options.insert(.input)
            }
            
            var heightInset: CGFloat = 0.0
            if case .forum = self.location {
                heightInset = 4.0
            }
            
            let bottomInset: CGFloat = layout.insets(options: options).bottom
            if !layout.safeInsets.left.isZero {
                tabBarHeight = 34.0 + bottomInset
                insets.bottom += 34.0
            } else {
                tabBarHeight = 49.0 - heightInset + bottomInset
                insets.bottom += 49.0 - heightInset
            }
            
            let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight))
            
            if let toolbarNode = self.toolbarNode {
                transition.updateFrame(node: toolbarNode, frame: toolbarFrame)
                toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: transition)
            } else {
                let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: self.presentationData.theme), displaySeparator: true, left: { [weak self] in
                    self?.toolbarActionSelected?(.left)
                }, right: { [weak self] in
                    self?.toolbarActionSelected?(.right)
                }, middle: { [weak self] in
                    self?.toolbarActionSelected?(.middle)
                })
                toolbarNode.frame = toolbarFrame
                toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: .immediate)
                self.addSubnode(toolbarNode)
                self.toolbarNode = toolbarNode
                if transition.isAnimated {
                    toolbarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
                }
            }
        } else if let toolbarNode = self.toolbarNode {
            self.toolbarNode = nil
            transition.updateAlpha(node: toolbarNode, alpha: 0.0, completion: { [weak toolbarNode] _ in
                toolbarNode?.removeFromSupernode()
            })
        }
        
        var childrenLayout = layout
        childrenLayout.intrinsicInsets = UIEdgeInsets(top: visualNavigationHeight, left: childrenLayout.intrinsicInsets.left, bottom: childrenLayout.intrinsicInsets.bottom, right: childrenLayout.intrinsicInsets.right)
        self.controller?.presentationContext.containerLayoutUpdated(childrenLayout, transition: transition)
        
        transition.updateFrame(node: self.mainContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size))
        var mainNavigationBarHeight = navigationBarHeight
        var cleanMainNavigationBarHeight = cleanNavigationBarHeight
        var mainInsets = insets
        if self.inlineStackContainerNode != nil && "".isEmpty {
            mainNavigationBarHeight = visualNavigationHeight
            cleanMainNavigationBarHeight = visualNavigationHeight
            mainInsets.top = visualNavigationHeight
        }
        self.mainContainerNode.update(layout: layout, navigationBarHeight: mainNavigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: cleanMainNavigationBarHeight, insets: mainInsets, isReorderingFilters: self.isReorderingFilters, isEditing: self.isEditing, inlineNavigationLocation: self.inlineStackContainerNode?.location, inlineNavigationTransitionFraction: self.inlineStackContainerTransitionFraction, storiesInset: storiesInset, transition: transition)
        
        if let inlineStackContainerNode = self.inlineStackContainerNode {
            var inlineStackContainerNodeTransition = transition
            var animateIn = false
            if inlineStackContainerNode.supernode == nil {
                self.insertSubnode(inlineStackContainerNode, aboveSubnode: self.mainContainerNode)
                inlineStackContainerNodeTransition = .immediate
                animateIn = true
            }
            
            let inlineSideInset: CGFloat = layout.safeInsets.left + 72.0
            var inlineStackFrame = CGRect(origin: CGPoint(x: inlineSideInset, y: 0.0), size: CGSize(width: layout.size.width - inlineSideInset, height: layout.size.height))
            inlineStackFrame.origin.x += (1.0 - self.inlineStackContainerTransitionFraction) * inlineStackFrame.width
            inlineStackContainerNodeTransition.updateFrame(node: inlineStackContainerNode, frame: inlineStackFrame)
            var inlineLayout = layout
            inlineLayout.size.width -= inlineSideInset
            inlineLayout.safeInsets.left = 0.0
            inlineLayout.intrinsicInsets.left = 0.0
            inlineLayout.additionalInsets.left = 0.0
            
            var inlineInsets = insets
            inlineInsets.left = 0.0
            
            let inlineNavigationHeight: CGFloat = navigationBarLayout.navigationHeight - navigationBarLayout.storiesInset
            
            inlineStackContainerNode.update(layout: inlineLayout, navigationBarHeight: inlineNavigationHeight, visualNavigationHeight: inlineNavigationHeight, originalNavigationHeight: inlineNavigationHeight, cleanNavigationBarHeight: inlineNavigationHeight, insets: inlineInsets, isReorderingFilters: self.isReorderingFilters, isEditing: self.isEditing, inlineNavigationLocation: nil, inlineNavigationTransitionFraction: 0.0, storiesInset: storiesInset, transition: inlineStackContainerNodeTransition)
            
            if animateIn {
                transition.animatePosition(node: inlineStackContainerNode, from: CGPoint(x: inlineStackContainerNode.position.x + inlineStackContainerNode.bounds.width + UIScreenPixel, y: inlineStackContainerNode.position.y))
            }
        }
        
        self.tapRecognizer?.isEnabled = self.isReorderingFilters
        
        if let searchDisplayController = self.searchDisplayController {
            if !self.skipSearchDisplayControllerLayout {
                searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: transition)
            }
        }
        
        self.updateNavigationScrolling(navigationHeight: navigationBarLayout.navigationHeight, transition: transition)
        
        if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
            navigationBarComponentView.deferScrollApplication = false
            navigationBarComponentView.applyCurrentScroll(transition: Transition(transition))
        }
    }
    
    func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) -> (ASDisplayNode, (Bool) -> Void)? {
        guard let (containerLayout, _, _, cleanNavigationBarHeight, _) = self.containerLayout, self.searchDisplayController == nil else {
            return nil
        }
        
        let effectiveLocation = self.inlineStackContainerNode?.location ?? self.location
        
        let filter: ChatListNodePeersFilter = []
        if case .forum = effectiveLocation {
            //filter.insert(.excludeRecent)
        }
        
        let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, requestPeerType: nil, location: effectiveLocation, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, threadId, dismissSearch in
            self?.requestOpenPeerFromSearch?(peer, threadId, dismissSearch)
        }, openDisabledPeer: { _, _ in
        }, openRecentPeerOptions: { [weak self] peer in
            self?.requestOpenRecentPeerOptions?(peer)
        }, openMessage: { [weak self] peer, threadId, messageId, deactivateOnAction in
            if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch {
                requestOpenMessageFromSearch(peer, threadId, messageId, deactivateOnAction)
            }
        }, addContact: { [weak self] phoneNumber in
            if let requestAddContact = self?.requestAddContact {
                requestAddContact(phoneNumber)
            }
        }, peerContextAction: self.peerContextAction, present: { [weak self] c, a in
            self?.controller?.present(c, in: .window(.root), with: a)
        }, presentInGlobalOverlay: { [weak self] c, a in
            self?.controller?.presentInGlobalOverlay(c, with: a)
        }, navigationController: navigationController, parentController: { [weak self] in
            return self?.controller
        })
        
        self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, contentNode: contentNode, cancel: { [weak self] in
            if let requestDeactivateSearch = self?.requestDeactivateSearch {
                requestDeactivateSearch()
            }
        })
        self.mainContainerNode.accessibilityElementsHidden = true
        self.inlineStackContainerNode?.accessibilityElementsHidden = true
                
        return (contentNode.filterContainerNode, { [weak self] focus in
            guard let strongSelf = self else {
                return
            }
            
            strongSelf.isSearchDisplayControllerActive = true
            
            strongSelf.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: cleanNavigationBarHeight, transition: .immediate)
            strongSelf.searchDisplayController?.activate(insertSubnode: { [weak self] subnode, isSearchBar in
                guard let self else {
                    return
                }
                
                if isSearchBar {
                    if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
                        navigationBarComponentView.addSubnode(subnode)
                    }
                } else {
                    self.insertSubnode(subnode, aboveSubnode: self.debugListView)
                }
            }, placeholder: placeholderNode, focus: focus)
            
            strongSelf.controller?.requestLayout(transition: .animated(duration: 0.5, curve: .spring))
        })
    }
    
    func deactivateSearch(placeholderNode: SearchBarPlaceholderNode, animated: Bool) -> (() -> Void)? {
        if let searchDisplayController = self.searchDisplayController {
            self.isSearchDisplayControllerActive = false
            self.searchDisplayController = nil
            self.mainContainerNode.accessibilityElementsHidden = false
            self.inlineStackContainerNode?.accessibilityElementsHidden = false
            
            return { [weak self, weak placeholderNode] in
                if let strongSelf = self, let placeholderNode, let (layout, _, _, cleanNavigationBarHeight, _) = strongSelf.containerLayout {
                    searchDisplayController.deactivate(placeholder: placeholderNode, animated: animated)
                    
                    searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: cleanNavigationBarHeight, transition: .animated(duration: 0.4, curve: .spring))
                    
                    strongSelf.controller?.requestLayout(transition: .animated(duration: 0.4, curve: .spring))
                }
            }
        } else {
            return nil
        }
    }
    
    func clearHighlightAnimated(_ animated: Bool) {
        self.mainContainerNode.currentItemNode.clearHighlightAnimated(true)
        self.inlineStackContainerNode?.currentItemNode.clearHighlightAnimated(true)
    }
    
    private var contentOffsetSyncLockedIn: Bool = false
    
    func willScrollToTop() {
        if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
            navigationBarComponentView.applyScroll(offset: 0.0, allowAvatarsExpansion: false, transition: Transition(animation: .curve(duration: 0.3, curve: .slide)))
        }
    }
    
    private func contentOffsetChanged(offset: ListViewVisibleContentOffset, listView: ListView, isPrimary: Bool) {
        guard let containerLayout = self.containerLayout else {
            return
        }
        self.updateNavigationScrolling(navigationHeight: containerLayout.navigationBarHeight, transition: self.tempNavigationScrollingTransition ?? .immediate)
        
        if listView.isDragging {
            var overscrollSelectedId: EnginePeer.Id?
            var overscrollHiddenChatItemsAllowed = false
            if let controller = self.controller, let componentView = controller.chatListHeaderView(), let storyPeerListView = componentView.storyPeerListView() {
                overscrollSelectedId = storyPeerListView.overscrollSelectedId
                overscrollHiddenChatItemsAllowed = storyPeerListView.overscrollHiddenChatItemsAllowed
            }
            
            if let chatListNode = listView as? ChatListNode {
                if chatListNode.hasItemsToBeRevealed() {
                    overscrollSelectedId = nil
                }
            }
            
            if let controller = self.controller {
                if let peerId = overscrollSelectedId {
                    if self.allowOverscrollStoryExpansion && self.inlineStackContainerNode == nil && isPrimary {
                        let timestamp = CACurrentMediaTime()
                        if let _ = self.currentOverscrollStoryExpansionTimestamp {
                        } else {
                            self.currentOverscrollStoryExpansionTimestamp = timestamp
                        }
                        
                        if let currentOverscrollStoryExpansionTimestamp = self.currentOverscrollStoryExpansionTimestamp, currentOverscrollStoryExpansionTimestamp <= timestamp - 0.0 {
                            self.allowOverscrollStoryExpansion = false
                            self.currentOverscrollStoryExpansionTimestamp = nil
                            self.allowOverscrollItemExpansion = false
                            self.currentOverscrollItemExpansionTimestamp = nil
                            HapticFeedback().tap()
                            
                            controller.openStories(peerId: peerId)
                        }
                    }
                } else {
                    if !overscrollHiddenChatItemsAllowed {
                        var manuallyAllow = false
                        
                        if isPrimary {
                            if let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) {
                            } else {
                                manuallyAllow = true
                            }
                        } else {
                            manuallyAllow = true
                        }
                        
                        if manuallyAllow, case let .known(value) = offset, value + listView.tempTopInset <= -40.0 {
                            overscrollHiddenChatItemsAllowed = true
                        }
                    }
                
                    if overscrollHiddenChatItemsAllowed {
                        if self.allowOverscrollItemExpansion {
                            let timestamp = CACurrentMediaTime()
                            if let _ = self.currentOverscrollItemExpansionTimestamp {
                            } else {
                                self.currentOverscrollItemExpansionTimestamp = timestamp
                            }
                            
                            if let currentOverscrollItemExpansionTimestamp = self.currentOverscrollItemExpansionTimestamp, currentOverscrollItemExpansionTimestamp <= timestamp - 0.0 {
                                self.allowOverscrollItemExpansion = false
                                
                                if isPrimary {
                                    self.mainContainerNode.currentItemNode.revealScrollHiddenItem()
                                } else {
                                    self.inlineStackContainerNode?.currentItemNode.revealScrollHiddenItem()
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    
    private func shouldStopScrolling(listView: ListView, velocity: CGFloat, isPrimary: Bool) -> Bool {
        if abs(velocity) > 0.8 {
            return false
        }
        
        if !isPrimary || self.inlineStackContainerNode == nil {
        } else {
            return false
        }
        
        guard let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View else {
            return false
        }
        
        if let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
            let searchScrollOffset = clippedScrollOffset
            if searchScrollOffset > 0.0 && searchScrollOffset < ChatListNavigationBar.searchScrollHeight {
                return true
            } else if clippedScrollOffset < 0.0 && clippedScrollOffset > -listView.tempTopInset {
                return true
            }
        }
        
        return false
    }
    
    private func didBeginInteractiveDragging(listView: ListView, isPrimary: Bool) {
        if isPrimary {
            if let chatListNode = listView as? ChatListNode, !chatListNode.hasItemsToBeRevealed() {
                self.allowOverscrollStoryExpansion = true
            } else {
                self.allowOverscrollStoryExpansion = false
            }
        }
        self.allowOverscrollItemExpansion = true
    }
    
    private func endedInteractiveDragging(listView: ListView, isPrimary: Bool) {
        if isPrimary {
            self.allowOverscrollStoryExpansion = false
            self.currentOverscrollStoryExpansionTimestamp = nil
        }
        self.allowOverscrollItemExpansion = false
        self.currentOverscrollItemExpansionTimestamp = nil
    }
    
    private func contentScrollingEnded(listView: ListView, isPrimary: Bool) -> Bool {
        if !isPrimary || self.inlineStackContainerNode == nil {
        } else {
            return false
        }
        
        guard let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View else {
            return false
        }
        
        if let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
            let searchScrollOffset = clippedScrollOffset
            if searchScrollOffset > 0.0 && searchScrollOffset < ChatListNavigationBar.searchScrollHeight {
                if searchScrollOffset < ChatListNavigationBar.searchScrollHeight * 0.5 {
                    let _ = listView.scrollToOffsetFromTop(0.0, animated: true)
                } else {
                    let _ = listView.scrollToOffsetFromTop(ChatListNavigationBar.searchScrollHeight, animated: true)
                }
                return true
            } else if clippedScrollOffset < 0.0 && clippedScrollOffset > -listView.tempTopInset {
                if navigationBarComponentView.storiesUnlocked {
                    let _ = listView.scrollToOffsetFromTop(-listView.tempTopInset, animated: true)
                } else {
                    let _ = listView.scrollToOffsetFromTop(0.0, animated: true)
                }
                return true
            }
        }
        
        return false
    }
    
    func makeInlineChatList(location: ChatListControllerLocation) -> ChatListContainerNode {
        var forumPeerId: EnginePeer.Id?
        if case let .forum(peerId) = location {
            forumPeerId = peerId
        }
        
        let inlineStackContainerNode = ChatListContainerNode(context: self.context, controller: self.controller, location: location, previewing: false, controlsHistoryPreload: false, isInlineMode: true, presentationData: self.presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filterBecameEmpty: { _ in }, filterEmptyAction: { [weak self] _ in self?.emptyListAction?(forumPeerId) }, secondaryEmptyAction: {}, openArchiveSettings: {})
        return inlineStackContainerNode
    }
    
    func setInlineChatList(inlineStackContainerNode: ChatListContainerNode?) {
        if let inlineStackContainerNode = inlineStackContainerNode {
            if self.inlineStackContainerNode !== inlineStackContainerNode {
                if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View {
                    if navigationBarComponentView.storiesUnlocked {
                        let _ = self.mainContainerNode.currentItemNode.scrollToOffsetFromTop(self.mainContainerNode.currentItemNode.tempTopInset, animated: true)
                    }
                }
                
                inlineStackContainerNode.leftSeparatorLayer.isHidden = false
                
                inlineStackContainerNode.presentAlert = self.mainContainerNode.presentAlert
                inlineStackContainerNode.present = self.mainContainerNode.present
                inlineStackContainerNode.push = self.mainContainerNode.push
                inlineStackContainerNode.deletePeerChat = self.mainContainerNode.deletePeerChat
                inlineStackContainerNode.deletePeerThread = self.mainContainerNode.deletePeerThread
                inlineStackContainerNode.setPeerThreadStopped = self.mainContainerNode.setPeerThreadStopped
                inlineStackContainerNode.setPeerThreadPinned = self.mainContainerNode.setPeerThreadPinned
                inlineStackContainerNode.setPeerThreadHidden = self.mainContainerNode.setPeerThreadHidden
                inlineStackContainerNode.peerSelected = self.mainContainerNode.peerSelected
                inlineStackContainerNode.groupSelected = self.mainContainerNode.groupSelected
                inlineStackContainerNode.updatePeerGrouping = self.mainContainerNode.updatePeerGrouping
                
                inlineStackContainerNode.contentOffsetChanged = { [weak self] offset, listView in
                    self?.contentOffsetChanged(offset: offset, listView: listView, isPrimary: false)
                }
                inlineStackContainerNode.didBeginInteractiveDragging = { [weak self] listView in
                    self?.didBeginInteractiveDragging(listView: listView, isPrimary: false)
                }
                inlineStackContainerNode.endedInteractiveDragging = { [weak self] listView in
                    self?.endedInteractiveDragging(listView: listView, isPrimary: false)
                }
                inlineStackContainerNode.shouldStopScrolling = { [weak self] listView, velocity in
                    return self?.shouldStopScrolling(listView: listView, velocity: velocity, isPrimary: false) ?? false
                }
                inlineStackContainerNode.contentScrollingEnded = { [weak self] listView in
                    return self?.contentScrollingEnded(listView: listView, isPrimary: false) ?? false
                }
                
                inlineStackContainerNode.activateChatPreview = self.mainContainerNode.activateChatPreview
                inlineStackContainerNode.openStories = self.mainContainerNode.openStories
                inlineStackContainerNode.addedVisibleChatsWithPeerIds = self.mainContainerNode.addedVisibleChatsWithPeerIds
                inlineStackContainerNode.didBeginSelectingChats = self.mainContainerNode.didBeginSelectingChats
                inlineStackContainerNode.displayFilterLimit = nil
                
                let previousInlineStackContainerNode = self.inlineStackContainerNode
                
                if let navigationBarComponentView = self.navigationBarView.view as? ChatListNavigationBar.View, let clippedScrollOffset = navigationBarComponentView.clippedScrollOffset {
                    let scrollOffset = max(0.0, clippedScrollOffset)
                    inlineStackContainerNode.initialScrollingOffset = scrollOffset
                }
                
                self.inlineStackContainerNode = inlineStackContainerNode
                self.inlineStackContainerTransitionFraction = 1.0
                
                if let _ = self.containerLayout {
                    let transition: ContainedViewLayoutTransition = .animated(duration: 0.5, curve: .spring)
                    
                    if let contentOffset = self.mainContainerNode.contentOffset, case let .known(offset) = contentOffset, offset < 0.0 {
                        if let containerLayout = self.containerLayout {
                            self.updateNavigationScrolling(navigationHeight: containerLayout.navigationBarHeight, transition: transition)
                            self.mainContainerNode.scrollToTop(animated: true, adjustForTempInset: false)
                        }
                    }
                    
                    if let previousInlineStackContainerNode {
                        transition.updatePosition(node: previousInlineStackContainerNode, position: CGPoint(x: previousInlineStackContainerNode.position.x + previousInlineStackContainerNode.bounds.width + UIScreenPixel, y: previousInlineStackContainerNode.position.y), completion: { [weak previousInlineStackContainerNode] _ in
                            previousInlineStackContainerNode?.removeFromSupernode()
                        })
                    }
                    
                    self.controller?.requestLayout(transition: transition)
                } else {
                    previousInlineStackContainerNode?.removeFromSupernode()
                }
            }
        } else {
            if let inlineStackContainerNode = self.inlineStackContainerNode {
                self.inlineStackContainerNode = nil
                self.inlineStackContainerTransitionFraction = 0.0
                
                if let _ = self.containerLayout {
                    let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
                    
                    transition.updatePosition(node: inlineStackContainerNode, position: CGPoint(x: inlineStackContainerNode.position.x + inlineStackContainerNode.bounds.width + UIScreenPixel, y: inlineStackContainerNode.position.y), completion: { [weak inlineStackContainerNode] _ in
                        inlineStackContainerNode?.removeFromSupernode()
                    })
                    
                    self.temporaryContentOffsetChangeTransition = transition
                    self.tempNavigationScrollingTransition = transition
                    self.controller?.requestLayout(transition: transition)
                    self.temporaryContentOffsetChangeTransition = nil
                    self.tempNavigationScrollingTransition = nil
                } else {
                    inlineStackContainerNode.removeFromSupernode()
                }
            }
        }
    }
    
    func playArchiveAnimation() {
        self.mainContainerNode.playArchiveAnimation()
    }
    
    func scrollToTop() {
        if let searchDisplayController = self.searchDisplayController {
            searchDisplayController.contentNode.scrollToTop()
        } else if let inlineStackContainerNode = self.inlineStackContainerNode {
            inlineStackContainerNode.scrollToTop(animated: true, adjustForTempInset: false)
        } else {
            self.mainContainerNode.scrollToTop(animated: true, adjustForTempInset: false)
        }
    }
    
    func scrollToTopIfStoriesAreExpanded() {
        if let contentOffset = self.mainContainerNode.contentOffset, case let .known(offset) = contentOffset, offset < 0.0 {
            self.mainContainerNode.scrollToTop(animated: true, adjustForTempInset: false)
            self.mainContainerNode.tempTopInset = 0.0
        }
    }
}

func shouldDisplayStoriesInChatListHeader(storySubscriptions: EngineStorySubscriptions, isHidden: Bool) -> Bool {
    if !storySubscriptions.items.isEmpty {
        return true
    }
    if !isHidden, let accountItem = storySubscriptions.accountItem {
        if accountItem.hasPending || accountItem.storyCount != 0 {
            return true
        }
    }
    return false
}