import Foundation import UIKit import SwiftSignalKit import TelegramPresentationData import AppBundle import AsyncDisplayKit import Postbox import SyncCore import TelegramCore import Display import AccountContext import SolidRoundedButtonNode import ItemListUI import ItemListPeerItem import SectionHeaderItem import TelegramStringFormatting import MergeLists import ContextUI import ShareController import OverlayStatusController import PresentationDataUtils import DirectionalPanGesture import UndoUI class InviteLinkInviteInteraction { let context: AccountContext let mainLinkContextAction: (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void let copyLink: (ExportedInvitation) -> Void let shareLink: (ExportedInvitation) -> Void let manageLinks: () -> Void init(context: AccountContext, mainLinkContextAction: @escaping (ExportedInvitation?, ASDisplayNode, ContextGesture?) -> Void, copyLink: @escaping (ExportedInvitation) -> Void, shareLink: @escaping (ExportedInvitation) -> Void, manageLinks: @escaping () -> Void) { self.context = context self.mainLinkContextAction = mainLinkContextAction self.copyLink = copyLink self.shareLink = shareLink self.manageLinks = manageLinks } } private struct InviteLinkInviteTransaction { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let isLoading: Bool } private enum InviteLinkInviteEntryId: Hashable { case header case mainLink case manage } private enum InviteLinkInviteEntry: Comparable, Identifiable { case header(PresentationTheme, String, String) case mainLink(PresentationTheme, ExportedInvitation) case manage(PresentationTheme, String, Bool) var stableId: InviteLinkInviteEntryId { switch self { case .header: return .header case .mainLink: return .mainLink case .manage: return .manage } } static func ==(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool { switch lhs { case let .header(lhsTheme, lhsTitle, lhsText): if case let .header(rhsTheme, rhsTitle, rhsText) = rhs, lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsText == rhsText { return true } else { return false } case let .mainLink(lhsTheme, lhsInvitation): if case let .mainLink(rhsTheme, rhsInvitation) = rhs, lhsTheme === rhsTheme, lhsInvitation == rhsInvitation { return true } else { return false } case let .manage(lhsTheme, lhsText, lhsStandalone): if case let .manage(rhsTheme, rhsText, rhsStandalone) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsStandalone == rhsStandalone { return true } else { return false } } } static func <(lhs: InviteLinkInviteEntry, rhs: InviteLinkInviteEntry) -> Bool { switch lhs { case .header: switch rhs { case .header: return false case .mainLink, .manage: return true } case .mainLink: switch rhs { case .header, .mainLink: return false case .manage: return true } case .manage: switch rhs { case .header, .mainLink: return false case .manage: return true } } } func item(account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> ListViewItem { switch self { case let .header(theme, title, text): return InviteLinkInviteHeaderItem(theme: theme, title: title, text: text) case let .mainLink(_, invite): return ItemListPermanentInviteLinkItem(context: interaction.context, presentationData: ItemListPresentationData(presentationData), invite: invite, count: 0, peers: [], displayButton: true, displayImporters: false, buttonColor: nil, sectionId: 0, style: .plain, copyAction: { interaction.copyLink(invite) }, shareAction: { interaction.shareLink(invite) }, contextAction: { node in interaction.mainLinkContextAction(invite, node, nil) }, viewAction: { }) case let .manage(theme, text, standalone): return InviteLinkInviteManageItem(theme: theme, text: text, standalone: standalone, action: { interaction.manageLinks() }) } } } private func preparedTransition(from fromEntries: [InviteLinkInviteEntry], to toEntries: [InviteLinkInviteEntry], isLoading: Bool, account: Account, presentationData: PresentationData, interaction: InviteLinkInviteInteraction) -> InviteLinkInviteTransaction { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, presentationData: presentationData, interaction: interaction), directionHint: nil) } return InviteLinkInviteTransaction(deletions: deletions, insertions: insertions, updates: updates, isLoading: isLoading) } public final class InviteLinkInviteController: ViewController { private var controllerNode: Node { return self.displayNode as! Node } private var animatedIn = false private let context: AccountContext private let peerId: PeerId private weak var parentNavigationController: NavigationController? private var presentationDataDisposable: Disposable? public init(context: AccountContext, peerId: PeerId, parentNavigationController: NavigationController?) { self.context = context self.peerId = peerId self.parentNavigationController = parentNavigationController super.init(navigationBarPresentationData: nil) self.navigationPresentation = .flatModal self.statusBar.statusBarStyle = .Ignore self.blocksBackgroundWhenInOverlay = true self.presentationDataDisposable = (context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { strongSelf.controllerNode.updatePresentationData(presentationData) } }) self.statusBar.statusBarStyle = .Ignore } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.presentationDataDisposable?.dispose() } override public func loadDisplayNode() { self.displayNode = Node(context: self.context, peerId: self.peerId, controller: self) } private var didAppearOnce: Bool = false private var isDismissed: Bool = false public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if !self.didAppearOnce { self.didAppearOnce = true self.controllerNode.animateIn() } } override public func dismiss(completion: (() -> Void)? = nil) { if !self.isDismissed { self.isDismissed = true self.didAppearOnce = false self.dismissAllTooltips() self.controllerNode.animateOut(completion: { [weak self] in completion?() self?.presentingViewController?.dismiss(animated: false, completion: nil) }) } } private func dismissAllTooltips() { self.window?.forEachController({ controller in if let controller = controller as? UndoOverlayController { controller.dismissWithCommitAction() } }) self.forEachController({ controller in if let controller = controller as? UndoOverlayController { controller.dismissWithCommitAction() } return true }) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.containerLayoutUpdated(layout, transition: transition) } class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private weak var controller: InviteLinkInviteController? private let context: AccountContext private let peerId: PeerId private let invitesContext: PeerExportedInvitationsContext private var interaction: InviteLinkInviteInteraction? private var presentationData: PresentationData private let presentationDataPromise: Promise private var disposable: Disposable? private let dimNode: ASDisplayNode private let contentNode: ASDisplayNode private let headerNode: ASDisplayNode private let headerBackgroundNode: ASDisplayNode private let titleNode: ImmediateTextNode private let doneButton: HighlightableButtonNode private let historyBackgroundNode: ASDisplayNode private let historyBackgroundContentNode: ASDisplayNode private var floatingHeaderOffset: CGFloat? private let listNode: ListView private var enqueuedTransitions: [InviteLinkInviteTransaction] = [] private var validLayout: ContainerViewLayout? private var presentationDataDisposable: Disposable? private var revokeDisposable = MetaDisposable() init(context: AccountContext, peerId: PeerId, controller: InviteLinkInviteController) { self.context = context self.peerId = peerId self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationDataPromise = Promise(self.presentationData) self.controller = controller self.invitesContext = PeerExportedInvitationsContext(account: context.account, peerId: peerId, adminId: nil, revoked: false, forceUpdate: false) self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.contentNode = ASDisplayNode() self.headerNode = ASDisplayNode() self.headerNode.clipsToBounds = true self.headerBackgroundNode = ASDisplayNode() self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.headerBackgroundNode.cornerRadius = 16.0 self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 1 self.titleNode.textAlignment = .center self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_InviteLink, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) self.doneButton = HighlightableButtonNode() self.doneButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) self.historyBackgroundNode = ASDisplayNode() self.historyBackgroundNode.isLayerBacked = true self.historyBackgroundContentNode = ASDisplayNode() self.historyBackgroundContentNode.isLayerBacked = true self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.historyBackgroundNode.addSubnode(self.historyBackgroundContentNode) self.listNode = ListView() self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3) self.listNode.verticalScrollIndicatorFollowsOverscroll = true super.init() self.backgroundColor = nil self.isOpaque = false self.interaction = InviteLinkInviteInteraction(context: context, mainLinkContextAction: { [weak self] invite, node, gesture in guard let node = node as? ContextExtractedContentContainingNode else { return } let presentationData = context.sharedContext.currentPresentationData.with { $0 } var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextCopy, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.dismissWithoutContent) if let invite = invite { UIPasteboard.general.string = invite.link self?.controller?.dismissAllTooltips() let presentationData = context.sharedContext.currentPresentationData.with { $0 } self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) } }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextGetQRCode, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Wallet/QrIcon"), color: theme.contextMenu.primaryColor) }, action: { _, f in f(.dismissWithoutContent) if let invite = invite { let _ = (context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { [weak self] peer in let isGroup: Bool if let peer = peer as? TelegramChannel, case .broadcast = peer.info { isGroup = false } else { isGroup = true } let controller = InviteLinkQRCodeController(context: context, invite: invite, isGroup: isGroup) self?.controller?.present(controller, in: .window(.root)) }) } }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.InviteLink_ContextRevoke, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { _, f in f(.dismissWithoutContent) let controller = ActionSheetController(presentationData: presentationData) let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetTextItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeAlert_Text), ActionSheetButtonItem(title: presentationData.strings.GroupInfo_InviteLink_RevokeLink, color: .destructive, action: { dismissAction() }) ]), ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) ]) self?.controller?.present(controller, in: .window(.root)) }))) let contextController = ContextController(account: context.account, presentationData: presentationData, source: .extracted(InviteLinkContextExtractedContentSource(controller: controller, sourceNode: node, blurBackground: false)), items: .single(items), reactionItems: [], gesture: gesture) self?.controller?.presentInGlobalOverlay(contextController) }, copyLink: { [weak self] invite in UIPasteboard.general.string = invite.link self?.controller?.dismissAllTooltips() let presentationData = context.sharedContext.currentPresentationData.with { $0 } self?.controller?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.InviteLink_InviteLinkCopiedText), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) }, shareLink: { [weak self] invite in let shareController = ShareController(context: context, subject: .url(invite.link)) self?.controller?.present(shareController, in: .window(.root)) }, manageLinks: { [weak self] in let controller = inviteLinkListController(context: context, peerId: peerId, admin: nil) self?.controller?.parentNavigationController?.pushViewController(controller) self?.controller?.dismiss() }) let previousEntries = Atomic<[InviteLinkInviteEntry]?>(value: nil) let peerView = context.account.postbox.peerView(id: peerId) let invites: Signal = .single(PeerExportedInvitationsState()) self.disposable = (combineLatest(self.presentationDataPromise.get(), peerView, invites) |> deliverOnMainQueue).start(next: { [weak self] presentationData, view, invites in if let strongSelf = self { var entries: [InviteLinkInviteEntry] = [] let helpText: String if let peer = peerViewMainPeer(view) as? TelegramChannel, case .broadcast = peer.info { helpText = presentationData.strings.InviteLink_CreatePrivateLinkHelpChannel } else { helpText = presentationData.strings.InviteLink_CreatePrivateLinkHelp } entries.append(.header(presentationData.theme, presentationData.strings.InviteLink_InviteLink, helpText)) let mainInvite: ExportedInvitation? if let cachedData = view.cachedData as? CachedGroupData, let invite = cachedData.exportedInvitation { mainInvite = invite } else if let cachedData = view.cachedData as? CachedChannelData, let invite = cachedData.exportedInvitation { mainInvite = invite } else { mainInvite = nil } if let mainInvite = mainInvite { entries.append(.mainLink(presentationData.theme, mainInvite)) } entries.append(.manage(presentationData.theme, presentationData.strings.InviteLink_Manage, true)) let previousEntries = previousEntries.swap(entries) let transition = preparedTransition(from: previousEntries ?? [], to: entries, isLoading: false, account: context.account, presentationData: presentationData, interaction: strongSelf.interaction!) strongSelf.enqueueTransition(transition) } }) self.listNode.preloadPages = true self.listNode.stackFromBottom = true self.listNode.updateFloatingHeaderOffset = { [weak self] offset, transition in if let strongSelf = self { strongSelf.updateFloatingHeaderOffset(offset: offset, transition: transition) } } self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in if case let .known(value) = offset, value < 40.0 { } } self.addSubnode(self.dimNode) self.addSubnode(self.contentNode) self.contentNode.addSubnode(self.historyBackgroundNode) self.contentNode.addSubnode(self.listNode) self.contentNode.addSubnode(self.headerNode) self.headerNode.addSubnode(self.headerBackgroundNode) self.headerNode.addSubnode(self.doneButton) self.doneButton.addTarget(self, action: #selector(self.doneButtonPressed), forControlEvents: .touchUpInside) self.presentationDataDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in if let strongSelf = self { if strongSelf.presentationData.theme !== presentationData.theme || strongSelf.presentationData.strings !== presentationData.strings { strongSelf.updatePresentationData(presentationData) } } }) } deinit { self.disposable?.dispose() self.presentationDataDisposable?.dispose() self.revokeDisposable.dispose() } override func didLoad() { super.didLoad() self.view.disablesInteractiveTransitionGestureRecognizer = true self.view.disablesInteractiveModalDismiss = true self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) let panRecognizer = DirectionalPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) panRecognizer.delegate = self panRecognizer.delaysTouchesBegan = false panRecognizer.cancelsTouchesInView = true self.view.addGestureRecognizer(panRecognizer) } @objc private func doneButtonPressed() { self.controller?.dismiss() } func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData self.presentationDataPromise.set(.single(presentationData)) self.historyBackgroundContentNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.headerBackgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor self.titleNode.attributedText = NSAttributedString(string: self.presentationData.strings.InviteLink_InviteLink, font: Font.bold(17.0), textColor: self.presentationData.theme.actionSheet.primaryTextColor) self.doneButton.setTitle(self.presentationData.strings.Common_Done, with: Font.bold(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal) } private func enqueueTransition(_ transition: InviteLinkInviteTransaction) { self.enqueuedTransitions.append(transition) if let _ = self.validLayout { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } } } private func dequeueTransition() { guard let _ = self.validLayout, let transition = self.enqueuedTransitions.first else { return } self.enqueuedTransitions.remove(at: 0) self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: ListViewDeleteAndInsertOptions(), updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in }) } func animateIn() { guard let layout = self.validLayout else { return } let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) let initialBounds = self.contentNode.bounds self.contentNode.bounds = initialBounds.offsetBy(dx: 0.0, dy: -layout.size.height) transition.animateView({ self.contentNode.view.bounds = initialBounds }) self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } func animateOut(completion: (() -> Void)?) { guard let layout = self.validLayout else { return } var offsetCompleted = false let internalCompletion: () -> Void = { if offsetCompleted { completion?() } } self.contentNode.layer.animateBoundsOriginYAdditive(from: self.contentNode.bounds.origin.y, to: -layout.size.height, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in offsetCompleted = true internalCompletion() }) self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size)) var insets = UIEdgeInsets() insets.left = layout.safeInsets.left insets.right = layout.safeInsets.right insets.bottom = layout.intrinsicInsets.bottom let headerHeight: CGFloat = 54.0 let visibleItemsHeight: CGFloat = 409.0 let layoutTopInset: CGFloat = max(layout.statusBarHeight ?? 0.0, layout.safeInsets.top) let listTopInset = layoutTopInset + headerHeight let listNodeSize = CGSize(width: layout.size.width, height: layout.size.height - listTopInset) insets.top = max(0.0, listNodeSize.height - visibleItemsHeight - insets.bottom) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: listNodeSize, insets: insets, duration: duration, curve: curve) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: listTopInset), size: listNodeSize)) transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 68.0)) let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width, height: headerHeight)) let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: 18.0), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) let doneSize = self.doneButton.measure(CGSize(width: layout.size.width, height: headerHeight)) let doneFrame = CGRect(origin: CGPoint(x: layout.size.width - doneSize.width - 16.0, y: 18.0), size: doneSize) transition.updateFrame(node: self.doneButton, frame: doneFrame) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) if result === self.headerNode.view { return self.view } if !self.bounds.contains(point) { return nil } if point.y < self.headerNode.frame.minY { return self.dimNode.view } return result } @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.controller?.dismiss() } } private var panGestureArguments: CGFloat? func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return gestureRecognizer is DirectionalPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer } @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { let contentOffset = self.listNode.visibleContentOffset() switch recognizer.state { case .began: self.panGestureArguments = 0.0 case .changed: var translation = recognizer.translation(in: self.contentNode.view).y if let currentOffset = self.panGestureArguments { if case let .known(value) = contentOffset, value <= 0.5 { if currentOffset > 0.0 { let translation = self.listNode.scroller.panGestureRecognizer.translation(in: self.listNode.scroller) if translation.y > 10.0 { self.listNode.scroller.panGestureRecognizer.isEnabled = false self.listNode.scroller.panGestureRecognizer.isEnabled = true } else { self.listNode.scroller.panGestureRecognizer.setTranslation(CGPoint(), in: self.listNode.scroller) } } } else { translation = 0.0 recognizer.setTranslation(CGPoint(), in: self.contentNode.view) } self.panGestureArguments = translation } var bounds = self.contentNode.bounds bounds.origin.y = -translation bounds.origin.y = min(0.0, bounds.origin.y) self.contentNode.bounds = bounds case .ended: let translation = recognizer.translation(in: self.contentNode.view) var velocity = recognizer.velocity(in: self.contentNode.view) if case let .known(value) = contentOffset, value > 0.0 { velocity = CGPoint() } else if case .unknown = contentOffset { velocity = CGPoint() } var bounds = self.contentNode.bounds bounds.origin.y = -translation.y bounds.origin.y = min(0.0, bounds.origin.y) self.panGestureArguments = nil if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) { self.controller?.dismiss() } else { var bounds = self.contentNode.bounds let previousBounds = bounds bounds.origin.y = 0.0 self.contentNode.bounds = bounds self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) } case .cancelled: self.panGestureArguments = nil let previousBounds = self.contentNode.bounds var bounds = self.contentNode.bounds bounds.origin.y = 0.0 self.contentNode.bounds = bounds self.contentNode.layer.animateBounds(from: previousBounds, to: self.contentNode.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) default: break } } private func updateFloatingHeaderOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { guard let validLayout = self.validLayout else { return } self.floatingHeaderOffset = offset let layoutTopInset: CGFloat = max(validLayout.statusBarHeight ?? 0.0, validLayout.safeInsets.top) let controlsHeight: CGFloat = 44.0 let listTopInset = layoutTopInset + controlsHeight let rawControlsOffset = offset + listTopInset - controlsHeight let controlsOffset = max(layoutTopInset, rawControlsOffset) let isOverscrolling = rawControlsOffset <= layoutTopInset let controlsFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsOffset), size: CGSize(width: validLayout.size.width, height: controlsHeight)) let previousFrame = self.headerNode.frame if !controlsFrame.equalTo(previousFrame) { self.headerNode.frame = controlsFrame let positionDelta = CGPoint(x: controlsFrame.minX - previousFrame.minX, y: controlsFrame.minY - previousFrame.minY) transition.animateOffsetAdditive(node: self.headerNode, offset: positionDelta.y) } // transition.updateAlpha(node: self.headerNode.separatorNode, alpha: isOverscrolling ? 1.0 : 0.0) let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: controlsFrame.maxY), size: CGSize(width: validLayout.size.width, height: validLayout.size.height)) let previousBackgroundFrame = self.historyBackgroundNode.frame if !backgroundFrame.equalTo(previousBackgroundFrame) { self.historyBackgroundNode.frame = backgroundFrame self.historyBackgroundContentNode.frame = CGRect(origin: CGPoint(), size: backgroundFrame.size) let positionDelta = CGPoint(x: backgroundFrame.minX - previousBackgroundFrame.minX, y: backgroundFrame.minY - previousBackgroundFrame.minY) transition.animateOffsetAdditive(node: self.historyBackgroundNode, offset: positionDelta.y) } } } }