import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import TelegramPresentationData import Postbox import TelegramCore import AccountContext import ContextUI import ChatControllerInteraction import PeerInfoVisualMediaPaneNode final class PeerInfoPaneWrapper { let key: PeerInfoPaneKey let node: PeerInfoPaneNode var isAnimatingOut: Bool = false private var appliedParams: (CGSize, CGFloat, CGFloat, CGFloat, CGFloat, Bool, CGFloat, PresentationData)? init(key: PeerInfoPaneKey, node: PeerInfoPaneNode) { self.key = key self.node = node } func update(size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData, synchronous: Bool, transition: ContainedViewLayoutTransition) { if let (currentSize, currentTopInset, currentSideInset, currentBottomInset, _, currentIsScrollingLockedAtTop, currentExpandProgress, currentPresentationData) = self.appliedParams { if currentSize == size && currentTopInset == topInset, currentSideInset == sideInset && currentBottomInset == bottomInset, currentIsScrollingLockedAtTop == isScrollingLockedAtTop && currentExpandProgress == expandProgress && currentPresentationData === presentationData { return } } self.appliedParams = (size, topInset, sideInset, bottomInset, visibleHeight, isScrollingLockedAtTop, expandProgress, presentationData) self.node.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: synchronous, transition: transition) } } final class PeerInfoPaneTabsContainerPaneNode: ASDisplayNode { private let pressed: () -> Void private let titleNode: ImmediateTextNode private let buttonNode: HighlightTrackingButtonNode private var isSelected: Bool = false init(pressed: @escaping () -> Void) { self.pressed = pressed self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false self.buttonNode = HighlightTrackingButtonNode() super.init() self.addSubnode(self.titleNode) self.addSubnode(self.buttonNode) self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) /*self.buttonNode.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted && !strongSelf.isSelected { strongSelf.titleNode.layer.removeAnimation(forKey: "opacity") strongSelf.titleNode.alpha = 0.4 } else { strongSelf.titleNode.alpha = 1.0 strongSelf.titleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } }*/ } @objc private func buttonPressed() { self.pressed() } func updateText(_ title: String, isSelected: Bool, presentationData: PresentationData) { self.isSelected = isSelected self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(14.0), textColor: isSelected ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor) self.buttonNode.accessibilityLabel = title self.buttonNode.accessibilityTraits = [.button] if isSelected { self.buttonNode.accessibilityTraits.insert(.selected) } } func updateLayout(height: CGFloat) -> CGFloat { let titleSize = self.titleNode.updateLayout(CGSize(width: 200.0, height: .greatestFiniteMagnitude)) self.titleNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((height - titleSize.height) / 2.0)), size: titleSize) return titleSize.width } func updateArea(size: CGSize, sideInset: CGFloat) { self.buttonNode.frame = CGRect(origin: CGPoint(x: -sideInset, y: 0.0), size: CGSize(width: size.width + sideInset * 2.0, height: size.height)) } } struct PeerInfoPaneSpecifier: Equatable { var key: PeerInfoPaneKey var title: String } private func interpolateFrame(from fromValue: CGRect, to toValue: CGRect, t: CGFloat) -> CGRect { return CGRect(x: floorToScreenPixels(toValue.origin.x * t + fromValue.origin.x * (1.0 - t)), y: floorToScreenPixels(toValue.origin.y * t + fromValue.origin.y * (1.0 - t)), width: floorToScreenPixels(toValue.size.width * t + fromValue.size.width * (1.0 - t)), height: floorToScreenPixels(toValue.size.height * t + fromValue.size.height * (1.0 - t))) } final class PeerInfoPaneTabsContainerNode: ASDisplayNode { private let scrollNode: ASScrollNode private var paneNodes: [PeerInfoPaneKey: PeerInfoPaneTabsContainerPaneNode] = [:] private let selectedLineNode: ASImageNode private var currentParams: ([PeerInfoPaneSpecifier], PeerInfoPaneKey?, PresentationData)? var requestSelectPane: ((PeerInfoPaneKey) -> Void)? override init() { self.scrollNode = ASScrollNode() self.selectedLineNode = ASImageNode() self.selectedLineNode.displaysAsynchronously = false self.selectedLineNode.displayWithoutProcessing = true super.init() self.scrollNode.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in guard let strongSelf = self else { return false } return strongSelf.scrollNode.view.contentOffset.x > .ulpOfOne } self.scrollNode.view.showsHorizontalScrollIndicator = false self.scrollNode.view.scrollsToTop = false if #available(iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.selectedLineNode) } func update(size: CGSize, presentationData: PresentationData, paneList: [PeerInfoPaneSpecifier], selectedPane: PeerInfoPaneKey?, transitionFraction: CGFloat, transition: ContainedViewLayoutTransition) { transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size)) let focusOnSelectedPane = self.currentParams?.1 != selectedPane if self.currentParams?.2.theme !== presentationData.theme { self.selectedLineNode.image = generateImage(CGSize(width: 7.0, height: 4.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(presentationData.theme.list.itemAccentColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.width))) })?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 1) } if self.currentParams?.0 != paneList || self.currentParams?.1 != selectedPane || self.currentParams?.2 !== presentationData { self.currentParams = (paneList, selectedPane, presentationData) for specifier in paneList { let paneNode: PeerInfoPaneTabsContainerPaneNode if let current = self.paneNodes[specifier.key] { paneNode = current } else { paneNode = PeerInfoPaneTabsContainerPaneNode(pressed: { [weak self] in self?.paneSelected(specifier.key) }) self.paneNodes[specifier.key] = paneNode } paneNode.updateText(specifier.title, isSelected: selectedPane == specifier.key, presentationData: presentationData) } var removeKeys: [PeerInfoPaneKey] = [] for (key, _) in self.paneNodes { if !paneList.contains(where: { $0.key == key }) { removeKeys.append(key) } } for key in removeKeys { if let paneNode = self.paneNodes.removeValue(forKey: key) { paneNode.removeFromSupernode() } } } var tabSizes: [(CGSize, PeerInfoPaneTabsContainerPaneNode, Bool)] = [] var totalRawTabSize: CGFloat = 0.0 var selectionFrames: [CGRect] = [] for specifier in paneList { guard let paneNode = self.paneNodes[specifier.key] else { continue } let wasAdded = paneNode.supernode == nil if wasAdded { self.scrollNode.addSubnode(paneNode) } let paneNodeWidth = paneNode.updateLayout(height: size.height) let paneNodeSize = CGSize(width: paneNodeWidth, height: size.height) tabSizes.append((paneNodeSize, paneNode, wasAdded)) totalRawTabSize += paneNodeSize.width } let minSpacing: CGFloat = 26.0 if tabSizes.count <= 1 { for i in 0 ..< tabSizes.count { let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] let leftOffset: CGFloat = 16.0 let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) if wasAdded { paneNode.frame = paneFrame paneNode.alpha = 0.0 transition.updateAlpha(node: paneNode, alpha: 1.0) } else { transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame) } let areaSideInset: CGFloat = 16.0 paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset) paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset) selectionFrames.append(paneFrame) } self.scrollNode.view.contentSize = CGSize(width: size.width, height: size.height) } else if totalRawTabSize + CGFloat(tabSizes.count + 1) * minSpacing <= size.width { let availableSpace = size.width let availableSpacing = availableSpace - totalRawTabSize let perTabSpacing = floor(availableSpacing / CGFloat(tabSizes.count + 1)) let normalizedPerTabWidth = floor(availableSpace / CGFloat(tabSizes.count)) var maxSpacing: CGFloat = 0.0 var minSpacing: CGFloat = .greatestFiniteMagnitude for i in 0 ..< tabSizes.count - 1 { let distanceToNextBoundary = (normalizedPerTabWidth - tabSizes[i].0.width) / 2.0 let nextDistanceToBoundary = (normalizedPerTabWidth - tabSizes[i + 1].0.width) / 2.0 let distance = nextDistanceToBoundary + distanceToNextBoundary maxSpacing = max(distance, maxSpacing) minSpacing = min(distance, minSpacing) } if minSpacing >= 100.0 || (maxSpacing / minSpacing) < 0.2 { for i in 0 ..< tabSizes.count { let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] let paneFrame = CGRect(origin: CGPoint(x: CGFloat(i) * normalizedPerTabWidth + floor((normalizedPerTabWidth - paneNodeSize.width) / 2.0), y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) if wasAdded { paneNode.frame = paneFrame paneNode.alpha = 0.0 transition.updateAlpha(node: paneNode, alpha: 1.0) } else { transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame) } let areaSideInset = floor((normalizedPerTabWidth - paneNodeSize.width) / 2.0) paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset) paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset) selectionFrames.append(paneFrame) } } else { var leftOffset = perTabSpacing for i in 0 ..< tabSizes.count { let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) if wasAdded { paneNode.frame = paneFrame paneNode.alpha = 0.0 transition.updateAlpha(node: paneNode, alpha: 1.0) } else { transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame) } let areaSideInset = floor(perTabSpacing / 2.0) paneNode.updateArea(size: paneFrame.size, sideInset: areaSideInset) paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -areaSideInset, bottom: 0.0, right: -areaSideInset) leftOffset += paneNodeSize.width + perTabSpacing selectionFrames.append(paneFrame) } } self.scrollNode.view.contentSize = CGSize(width: size.width, height: size.height) } else { let sideInset: CGFloat = 16.0 var leftOffset: CGFloat = sideInset for i in 0 ..< tabSizes.count { let (paneNodeSize, paneNode, wasAdded) = tabSizes[i] let paneFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - paneNodeSize.height) / 2.0)), size: paneNodeSize) if wasAdded { paneNode.frame = paneFrame paneNode.alpha = 0.0 transition.updateAlpha(node: paneNode, alpha: 1.0) } else { transition.updateFrameAdditiveToCenter(node: paneNode, frame: paneFrame) } paneNode.updateArea(size: paneFrame.size, sideInset: minSpacing) paneNode.hitTestSlop = UIEdgeInsets(top: 0.0, left: -minSpacing, bottom: 0.0, right: -minSpacing) selectionFrames.append(paneFrame) leftOffset += paneNodeSize.width + minSpacing } self.scrollNode.view.contentSize = CGSize(width: leftOffset - minSpacing + sideInset, height: size.height) } var selectedFrame: CGRect? if let selectedPane = selectedPane, let currentIndex = paneList.firstIndex(where: { $0.key == selectedPane }) { if currentIndex != 0 && transitionFraction > 0.0 { let currentFrame = selectionFrames[currentIndex] let previousFrame = selectionFrames[currentIndex - 1] selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction)) } else if currentIndex != paneList.count - 1 && transitionFraction < 0.0 { let currentFrame = selectionFrames[currentIndex] let previousFrame = selectionFrames[currentIndex + 1] selectedFrame = interpolateFrame(from: currentFrame, to: previousFrame, t: abs(transitionFraction)) } else { selectedFrame = selectionFrames[currentIndex] } } if let selectedFrame = selectedFrame { let wasAdded = self.selectedLineNode.isHidden self.selectedLineNode.isHidden = false let lineFrame = CGRect(origin: CGPoint(x: selectedFrame.minX, y: size.height - 4.0), size: CGSize(width: selectedFrame.width, height: 4.0)) if wasAdded { self.selectedLineNode.frame = lineFrame self.selectedLineNode.alpha = 0.0 transition.updateAlpha(node: self.selectedLineNode, alpha: 1.0) } else { transition.updateFrame(node: self.selectedLineNode, frame: lineFrame) } if focusOnSelectedPane { if selectedPane == paneList.first?.key { transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)) } else if selectedPane == paneList.last?.key { transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: max(0.0, self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width), y: 0.0), size: self.scrollNode.bounds.size)) } else { let contentOffsetX = max(0.0, min(self.scrollNode.view.contentSize.width - self.scrollNode.bounds.width, floor(selectedFrame.midX - self.scrollNode.bounds.width / 2.0))) transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: self.scrollNode.bounds.size)) } } } else { self.selectedLineNode.isHidden = true } } private func paneSelected(_ key: PeerInfoPaneKey) { self.requestSelectPane?(key) } } private final class PeerInfoPendingPane { let pane: PeerInfoPaneWrapper private var disposable: Disposable? var isReady: Bool = false init( context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, chatControllerInteraction: ChatControllerInteraction, data: PeerInfoScreenData, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void, openAddMemberAction: @escaping () -> Void, requestPerformPeerMemberAction: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, key: PeerInfoPaneKey, hasBecomeReady: @escaping (PeerInfoPaneKey) -> Void, parentController: ViewController?, openMediaCalendar: @escaping () -> Void, paneDidScroll: @escaping () -> Void ) { let captureProtected = data.peer?.isCopyProtectionEnabled ?? false let paneNode: PeerInfoPaneNode switch key { case .stories: let visualPaneNode = PeerInfoStoryPaneNode(context: context, peerId: peerId, chatLocation: chatLocation, contentType: .photoOrVideo, captureProtected: captureProtected, isSaved: false, isArchive: false, navigationController: chatControllerInteraction.navigationController) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() } visualPaneNode.paneDidScroll = { paneDidScroll() } case .media: let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .photoOrVideo, captureProtected: captureProtected) paneNode = visualPaneNode visualPaneNode.openCurrentDate = { openMediaCalendar() } visualPaneNode.paneDidScroll = { paneDidScroll() } case .files: let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .files, captureProtected: captureProtected) paneNode = visualPaneNode //paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, tagMask: .file) case .links: paneNode = PeerInfoListPaneNode(context: context, updatedPresentationData: updatedPresentationData, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, tagMask: .webPage) case .voice: let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .voiceAndVideoMessages, captureProtected: captureProtected) paneNode = visualPaneNode case .music: let visualPaneNode = PeerInfoVisualMediaPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .music, captureProtected: captureProtected) paneNode = visualPaneNode case .gifs: let visualPaneNode = PeerInfoGifPaneNode(context: context, chatControllerInteraction: chatControllerInteraction, peerId: peerId, chatLocation: chatLocation, chatLocationContextHolder: chatLocationContextHolder, contentType: .gifs) paneNode = visualPaneNode case .groupsInCommon: paneNode = PeerInfoGroupsInCommonPaneNode(context: context, peerId: peerId, chatControllerInteraction: chatControllerInteraction, openPeerContextAction: openPeerContextAction, groupsInCommonContext: data.groupsInCommon!) case .members: if case let .longList(membersContext) = data.members { paneNode = PeerInfoMembersPaneNode(context: context, peerId: peerId, membersContext: membersContext, addMemberAction: { openAddMemberAction() }, action: { member, action in requestPerformPeerMemberAction(member, action) }) } else { preconditionFailure() } } paneNode.parentController = parentController self.pane = PeerInfoPaneWrapper(key: key, node: paneNode) self.disposable = (paneNode.isReady |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in self?.isReady = true hasBecomeReady(key) }) } deinit { self.disposable?.dispose() } } final class PeerInfoPaneContainerNode: ASDisplayNode, UIGestureRecognizerDelegate { private let context: AccountContext private let peerId: PeerId private let chatLocation: ChatLocation private let chatLocationContextHolder: Atomic private let isMediaOnly: Bool weak var parentController: ViewController? private let coveringBackgroundNode: NavigationBackgroundNode private let additionalBackgroundNode: ASDisplayNode private let separatorNode: ASDisplayNode private let tabsContainerNode: PeerInfoPaneTabsContainerNode private let tabsSeparatorNode: ASDisplayNode let isReady = Promise() var didSetIsReady = false private var currentParams: (size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?)? private(set) var currentPaneKey: PeerInfoPaneKey? var pendingSwitchToPaneKey: PeerInfoPaneKey? var expandOnSwitch = false var currentPane: PeerInfoPaneWrapper? { if let currentPaneKey = self.currentPaneKey { return self.currentPanes[currentPaneKey] } else { return nil } } private let currentPaneStatusPromise = Promise(nil) private let nextPaneStatusPromise = Promise(nil) private let paneTransitionPromise = ValuePromise(nil) var currentPaneStatus: Signal<(PeerInfoStatusData?, PeerInfoStatusData?, CGFloat?), NoError> { return combineLatest(queue: Queue.mainQueue(), self.currentPaneStatusPromise.get(), self.nextPaneStatusPromise.get(), self.paneTransitionPromise.get()) } private var currentPanes: [PeerInfoPaneKey: PeerInfoPaneWrapper] = [:] private var pendingPanes: [PeerInfoPaneKey: PeerInfoPendingPane] = [:] private var shouldFadeIn = false private var transitionFraction: CGFloat = 0.0 var selectionPanelNode: PeerInfoSelectionPanelNode? var chatControllerInteraction: ChatControllerInteraction? var openPeerContextAction: ((Peer, ASDisplayNode, ContextGesture?) -> Void)? var openAddMemberAction: (() -> Void)? var requestPerformPeerMemberAction: ((PeerInfoMember, PeerMembersListAction) -> Void)? var currentPaneUpdated: ((Bool) -> Void)? var requestExpandTabs: (() -> Bool)? var openMediaCalendar: (() -> Void)? var paneDidScroll: (() -> Void)? private var currentAvailablePanes: [PeerInfoPaneKey]? private let updatedPresentationData: (initial: PresentationData, signal: Signal)? init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, peerId: PeerId, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, isMediaOnly: Bool) { self.context = context self.updatedPresentationData = updatedPresentationData self.peerId = peerId self.chatLocation = chatLocation self.chatLocationContextHolder = chatLocationContextHolder self.isMediaOnly = isMediaOnly self.additionalBackgroundNode = ASDisplayNode() self.separatorNode = ASDisplayNode() self.separatorNode.isLayerBacked = true self.coveringBackgroundNode = NavigationBackgroundNode(color: .clear) self.coveringBackgroundNode.isUserInteractionEnabled = false self.tabsContainerNode = PeerInfoPaneTabsContainerNode() self.tabsSeparatorNode = ASDisplayNode() self.tabsSeparatorNode.isLayerBacked = true super.init() // self.addSubnode(self.separatorNode) self.addSubnode(self.additionalBackgroundNode) self.addSubnode(self.coveringBackgroundNode) self.addSubnode(self.tabsContainerNode) self.addSubnode(self.tabsSeparatorNode) self.tabsContainerNode.requestSelectPane = { [weak self] key in guard let strongSelf = self else { return } if strongSelf.currentPaneKey == key { if let requestExpandTabs = strongSelf.requestExpandTabs, requestExpandTabs() { } else { let _ = strongSelf.currentPane?.node.scrollToTop() } return } if strongSelf.currentPanes[key] != nil { strongSelf.currentPaneKey = key if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.4, curve: .spring)) strongSelf.currentPaneUpdated?(true) strongSelf.currentPaneStatusPromise.set(strongSelf.currentPane?.node.status ?? .single(nil)) strongSelf.nextPaneStatusPromise.set(.single(nil)) strongSelf.paneTransitionPromise.set(nil) } } else if strongSelf.pendingSwitchToPaneKey != key { strongSelf.pendingSwitchToPaneKey = key strongSelf.expandOnSwitch = true if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.4, curve: .spring)) } } } } override func didLoad() { super.didLoad() let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in guard let strongSelf = self, let currentPaneKey = strongSelf.currentPaneKey, let availablePanes = strongSelf.currentParams?.data?.availablePanes, let index = availablePanes.firstIndex(of: currentPaneKey) else { return [] } if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) { return [] } if index == 0 { return .left } return [.left, .right] }) panRecognizer.delegate = self panRecognizer.delaysTouchesBegan = false panRecognizer.cancelsTouchesInView = true self.view.addGestureRecognizer(panRecognizer) } 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 panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: func cancelContextGestures(view: UIView) { if let gestureRecognizers = view.gestureRecognizers { for gesture in gestureRecognizers { if let gesture = gesture as? ContextGesture { gesture.cancel() } } } for subview in view.subviews { cancelContextGestures(view: subview) } } cancelContextGestures(view: self.view) case .changed: if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) { let translation = recognizer.translation(in: self.view) var transitionFraction = translation.x / size.width if currentIndex <= 0 { transitionFraction = min(0.0, transitionFraction) } if currentIndex >= availablePanes.count - 1 { transitionFraction = max(0.0, transitionFraction) } self.transitionFraction = transitionFraction // let nextKey = availablePanes[updatedIndex] // print(transitionFraction) self.paneTransitionPromise.set(transitionFraction) self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate) self.currentPaneUpdated?(false) } case .cancelled, .ended: if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = self.currentParams, let availablePanes = data?.availablePanes, availablePanes.count > 1, let currentPaneKey = self.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey) { let translation = recognizer.translation(in: self.view) let velocity = recognizer.velocity(in: self.view) var directionIsToRight: Bool? if abs(velocity.x) > 10.0 { directionIsToRight = velocity.x < 0.0 } else { if abs(translation.x) > size.width / 2.0 { directionIsToRight = translation.x > size.width / 2.0 } } if let directionIsToRight = directionIsToRight { var updatedIndex = currentIndex if directionIsToRight { updatedIndex = min(updatedIndex + 1, availablePanes.count - 1) } else { updatedIndex = max(updatedIndex - 1, 0) } let switchToKey = availablePanes[updatedIndex] if switchToKey != self.currentPaneKey && self.currentPanes[switchToKey] != nil{ self.currentPaneKey = switchToKey } } self.transitionFraction = 0.0 self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .animated(duration: 0.35, curve: .spring)) self.currentPaneUpdated?(false) self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil)) } default: break } } func scrollToTop() -> Bool { if let currentPane = self.currentPane { return currentPane.node.scrollToTop() } else { return false } } func findLoadedMessage(id: MessageId) -> Message? { return self.currentPane?.node.findLoadedMessage(id: id) } func updateHiddenMedia() { self.currentPane?.node.updateHiddenMedia() } func transitionNodeForGallery(messageId: MessageId, media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { return self.currentPane?.node.transitionNodeForGallery(messageId: messageId, media: media) } func updateSelectedMessageIds(_ selectedMessageIds: Set?, animated: Bool) { for (_, pane) in self.currentPanes { pane.node.updateSelectedMessages(animated: animated) } for (_, pane) in self.pendingPanes { pane.pane.node.updateSelectedMessages(animated: animated) } } func update(size: CGSize, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, expansionFraction: CGFloat, presentationData: PresentationData, data: PeerInfoScreenData?, transition: ContainedViewLayoutTransition) { let previousAvailablePanes = self.currentAvailablePanes let availablePanes = data?.availablePanes ?? [] self.currentAvailablePanes = data?.availablePanes let previousPaneKeys = Set(self.currentPanes.keys) let previousCurrentPaneKey = self.currentPaneKey var updateCurrentPaneStatus = false if let currentPaneKey = self.currentPaneKey, !availablePanes.contains(currentPaneKey) { var nextCandidatePaneKey: PeerInfoPaneKey? if let previousAvailablePanes = previousAvailablePanes, let index = previousAvailablePanes.firstIndex(of: currentPaneKey), index != 0 { for i in (0 ... index - 1).reversed() { if availablePanes.contains(previousAvailablePanes[i]) { nextCandidatePaneKey = previousAvailablePanes[i] } } } if nextCandidatePaneKey == nil { nextCandidatePaneKey = availablePanes.first } if let nextCandidatePaneKey = nextCandidatePaneKey { self.pendingSwitchToPaneKey = nextCandidatePaneKey } else { self.currentPaneKey = nil self.pendingSwitchToPaneKey = nil } } else if self.currentPaneKey == nil { self.pendingSwitchToPaneKey = availablePanes.first } let currentIndex: Int? if let currentPaneKey = self.currentPaneKey { currentIndex = availablePanes.firstIndex(of: currentPaneKey) } else { currentIndex = nil } self.currentParams = (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) transition.updateAlpha(node: self.coveringBackgroundNode, alpha: expansionFraction) // transition.updateAlpha(node: self.additionalBackgroundNode, alpha: 1.0 - expansionFraction) self.backgroundColor = presentationData.theme.list.plainBackgroundColor self.coveringBackgroundNode.updateColor(color: presentationData.theme.rootController.navigationBar.opaqueBackgroundColor, transition: .immediate) self.separatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor self.additionalBackgroundNode.backgroundColor = presentationData.theme.list.itemBlocksBackgroundColor self.tabsSeparatorNode.backgroundColor = presentationData.theme.list.itemBlocksSeparatorColor let isScrollingLockedAtTop = expansionFraction < 1.0 - CGFloat.ulpOfOne let tabsHeight: CGFloat = 48.0 let paneFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) var visiblePaneIndices: [Int] = [] var requiredPendingKeys: [PeerInfoPaneKey] = [] if let currentIndex = currentIndex { if currentIndex != 0 { visiblePaneIndices.append(currentIndex - 1) } visiblePaneIndices.append(currentIndex) if currentIndex != availablePanes.count - 1 { visiblePaneIndices.append(currentIndex + 1) } for index in visiblePaneIndices { let key = availablePanes[index] if self.currentPanes[key] == nil && self.pendingPanes[key] == nil { requiredPendingKeys.append(key) } } } if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey { if self.currentPanes[pendingSwitchToPaneKey] == nil && self.pendingPanes[pendingSwitchToPaneKey] == nil { if !requiredPendingKeys.contains(pendingSwitchToPaneKey) { requiredPendingKeys.append(pendingSwitchToPaneKey) } } } for key in requiredPendingKeys { if self.pendingPanes[key] == nil { var leftScope = false let pane = PeerInfoPendingPane( context: self.context, updatedPresentationData: self.updatedPresentationData, chatControllerInteraction: self.chatControllerInteraction!, data: data!, openPeerContextAction: { [weak self] peer, node, gesture in self?.openPeerContextAction?(peer, node, gesture) }, openAddMemberAction: { [weak self] in self?.openAddMemberAction?() }, requestPerformPeerMemberAction: { [weak self] member, action in self?.requestPerformPeerMemberAction?(member, action) }, peerId: self.peerId, chatLocation: self.chatLocation, chatLocationContextHolder: self.chatLocationContextHolder, key: key, hasBecomeReady: { [weak self] key in let apply: () -> Void = { guard let strongSelf = self else { return } if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { var transition: ContainedViewLayoutTransition = .immediate if strongSelf.pendingSwitchToPaneKey == key && strongSelf.currentPaneKey != nil { transition = .animated(duration: 0.4, curve: .spring) } strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: transition) } } if leftScope { apply() } }, parentController: self.parentController, openMediaCalendar: { [weak self] in self?.openMediaCalendar?() }, paneDidScroll: { [weak self] in self?.paneDidScroll?() } ) self.pendingPanes[key] = pane pane.pane.node.frame = paneFrame pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: .immediate) let paneNode = pane.pane.node pane.pane.node.tabBarOffsetUpdated = { [weak self, weak paneNode] transition in guard let strongSelf = self, let paneNode = paneNode, let currentPane = strongSelf.currentPane, paneNode === currentPane.node else { return } if let (size, sideInset, bottomInset, visibleHeight, expansionFraction, presentationData, data) = strongSelf.currentParams { strongSelf.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: transition) } } leftScope = true } } for (key, pane) in self.pendingPanes { pane.pane.node.frame = paneFrame pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: self.currentPaneKey == nil, transition: .immediate) if pane.isReady { self.pendingPanes.removeValue(forKey: key) self.currentPanes[key] = pane.pane } } var paneDefaultTransition = transition var previousPaneKey: PeerInfoPaneKey? var paneSwitchAnimationOffset: CGFloat = 0.0 var updatedCurrentIndex = currentIndex if let pendingSwitchToPaneKey = self.pendingSwitchToPaneKey, let _ = self.currentPanes[pendingSwitchToPaneKey] { self.pendingSwitchToPaneKey = nil previousPaneKey = self.currentPaneKey self.currentPaneKey = pendingSwitchToPaneKey updateCurrentPaneStatus = true updatedCurrentIndex = availablePanes.firstIndex(of: pendingSwitchToPaneKey) if let previousPaneKey = previousPaneKey, let previousIndex = availablePanes.firstIndex(of: previousPaneKey), let updatedCurrentIndex = updatedCurrentIndex { if updatedCurrentIndex < previousIndex { paneSwitchAnimationOffset = -size.width } else { paneSwitchAnimationOffset = size.width } } paneDefaultTransition = .immediate } if let _ = data { if let previousAvailablePanes = previousAvailablePanes, previousAvailablePanes.isEmpty, !availablePanes.isEmpty { self.shouldFadeIn = true self.alpha = 0.0 } let currentPaneKeys = Set(self.currentPanes.keys) if previousPaneKeys.isEmpty && !currentPaneKeys.isEmpty && self.shouldFadeIn { self.shouldFadeIn = false self.alpha = 1.0 self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } } for (key, pane) in self.currentPanes { if let index = availablePanes.firstIndex(of: key), let updatedCurrentIndex = updatedCurrentIndex { var paneWasAdded = false if pane.node.supernode == nil { self.insertSubnode(pane.node, belowSubnode: self.coveringBackgroundNode) paneWasAdded = true } let indexOffset = CGFloat(index - updatedCurrentIndex) let paneTransition: ContainedViewLayoutTransition = paneWasAdded ? .immediate : paneDefaultTransition let adjustedFrame = paneFrame.offsetBy(dx: size.width * self.transitionFraction + indexOffset * size.width, dy: 0.0) let paneCompletion: () -> Void = { [weak self, weak pane] in guard let strongSelf = self, let pane = pane else { return } pane.isAnimatingOut = false if let (_, _, _, _, _, _, data) = strongSelf.currentParams { if let availablePanes = data?.availablePanes, let currentPaneKey = strongSelf.currentPaneKey, let currentIndex = availablePanes.firstIndex(of: currentPaneKey), let paneIndex = availablePanes.firstIndex(of: key), abs(paneIndex - currentIndex) <= 1 { } else { if let pane = strongSelf.currentPanes.removeValue(forKey: key) { //print("remove \(key)") pane.node.removeFromSupernode() } } } } if let previousPaneKey = previousPaneKey, key == previousPaneKey { pane.node.frame = adjustedFrame let isAnimatingOut = pane.isAnimatingOut pane.isAnimatingOut = true transition.animateFrame(node: pane.node, from: paneFrame, to: paneFrame.offsetBy(dx: -paneSwitchAnimationOffset, dy: 0.0), completion: isAnimatingOut ? nil : { _ in paneCompletion() }) } else if let _ = previousPaneKey, key == self.currentPaneKey { pane.node.frame = adjustedFrame let isAnimatingOut = pane.isAnimatingOut pane.isAnimatingOut = true transition.animatePositionAdditive(node: pane.node, offset: CGPoint(x: paneSwitchAnimationOffset, y: 0.0), completion: isAnimatingOut ? nil : { paneCompletion() }) } else { let isAnimatingOut = pane.isAnimatingOut pane.isAnimatingOut = true paneTransition.updateFrame(node: pane.node, frame: adjustedFrame, completion: isAnimatingOut ? nil : { _ in paneCompletion() }) } pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: paneWasAdded, transition: paneTransition) } } var tabsOffset: CGFloat = 0.0 if let currentPane = self.currentPane { tabsOffset = currentPane.node.tabBarOffset } tabsOffset = max(0.0, min(tabsHeight, tabsOffset)) if isScrollingLockedAtTop || self.isMediaOnly { tabsOffset = 0.0 } var tabsAlpha = 1.0 - tabsOffset / tabsHeight tabsAlpha *= tabsAlpha transition.updateFrame(node: self.tabsContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -tabsOffset), size: CGSize(width: size.width, height: tabsHeight))) transition.updateAlpha(node: self.tabsContainerNode, alpha: tabsAlpha) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel))) transition.updateFrame(node: self.coveringBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel))) transition.updateFrame(node: self.additionalBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel - tabsOffset), size: CGSize(width: size.width, height: tabsHeight + UIScreenPixel))) self.coveringBackgroundNode.update(size: self.coveringBackgroundNode.bounds.size, transition: transition) transition.updateFrame(node: self.tabsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: tabsHeight - tabsOffset), size: CGSize(width: size.width, height: UIScreenPixel))) self.tabsContainerNode.update(size: CGSize(width: size.width, height: tabsHeight), presentationData: presentationData, paneList: availablePanes.map { key in let title: String switch key { case .stories: //TODO:localize title = "Stories" case .media: title = presentationData.strings.PeerInfo_PaneMedia case .files: title = presentationData.strings.PeerInfo_PaneFiles case .links: title = presentationData.strings.PeerInfo_PaneLinks case .voice: title = presentationData.strings.PeerInfo_PaneVoiceAndVideo case .gifs: title = presentationData.strings.PeerInfo_PaneGifs case .music: title = presentationData.strings.PeerInfo_PaneAudio case .groupsInCommon: title = presentationData.strings.PeerInfo_PaneGroups case .members: title = presentationData.strings.PeerInfo_PaneMembers } return PeerInfoPaneSpecifier(key: key, title: title) }, selectedPane: self.currentPaneKey, transitionFraction: self.transitionFraction, transition: transition) for (_, pane) in self.pendingPanes { let paneTransition: ContainedViewLayoutTransition = .immediate paneTransition.updateFrame(node: pane.pane.node, frame: paneFrame) pane.pane.update(size: paneFrame.size, topInset: tabsHeight, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expansionFraction, presentationData: presentationData, synchronous: true, transition: paneTransition) } if !self.didSetIsReady && data != nil { if let currentPaneKey = self.currentPaneKey, let currentPane = self.currentPanes[currentPaneKey] { self.didSetIsReady = true self.isReady.set(currentPane.node.isReady) } else if self.pendingSwitchToPaneKey == nil { self.didSetIsReady = true self.isReady.set(.single(true)) } } if let previousCurrentPaneKey = previousCurrentPaneKey, self.currentPaneKey != previousCurrentPaneKey { self.currentPaneUpdated?(self.expandOnSwitch) self.expandOnSwitch = false } if updateCurrentPaneStatus { self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil)) } } }