mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1479 lines
75 KiB
Swift
1479 lines
75 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SafariServices
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import AccountContext
|
|
import ShareController
|
|
import SaveToCameraRoll
|
|
import GalleryUI
|
|
import OpenInExternalAppUI
|
|
import LocationUI
|
|
import UndoUI
|
|
import ContextUI
|
|
import Translate
|
|
|
|
final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|
private weak var controller: InstantPageController?
|
|
private let context: AccountContext
|
|
private var settings: InstantPagePresentationSettings?
|
|
private var themeSettings: PresentationThemeSettings?
|
|
private var presentationTheme: PresentationTheme
|
|
private var strings: PresentationStrings
|
|
private var nameDisplayOrder: PresentationPersonNameOrder
|
|
private let autoNightModeTriggered: Bool
|
|
private var dateTimeFormat: PresentationDateTimeFormat
|
|
private var theme: InstantPageTheme?
|
|
private let sourcePeerType: MediaAutoDownloadPeerType
|
|
private var manualThemeOverride: InstantPageThemeType?
|
|
private let getNavigationController: () -> NavigationController?
|
|
private let present: (ViewController, Any?) -> Void
|
|
private let pushController: (ViewController) -> Void
|
|
private let openPeer: (PeerId) -> Void
|
|
|
|
private var webPage: TelegramMediaWebpage?
|
|
private var initialAnchor: String?
|
|
private var pendingAnchor: String?
|
|
private var initialState: InstantPageStoredState?
|
|
|
|
private var containerLayout: ContainerViewLayout?
|
|
private var setupScrollOffsetOnLayout: Bool = false
|
|
|
|
private let statusBar: StatusBar
|
|
private let navigationBar: InstantPageNavigationBar
|
|
private let scrollNode: ASScrollNode
|
|
private let scrollNodeHeader: ASDisplayNode
|
|
private let scrollNodeFooter: ASDisplayNode
|
|
private var linkHighlightingNode: LinkHighlightingNode?
|
|
private var textSelectionNode: LinkHighlightingNode?
|
|
private var settingsNode: InstantPageSettingsNode?
|
|
private var settingsDimNode: ASDisplayNode?
|
|
|
|
var currentLayout: InstantPageLayout?
|
|
var currentLayoutTiles: [InstantPageTile] = []
|
|
var currentLayoutItemsWithNodes: [InstantPageItem] = []
|
|
var distanceThresholdGroupCount: [Int: Int] = [:]
|
|
|
|
var visibleTiles: [Int: InstantPageTileNode] = [:]
|
|
var visibleItemsWithNodes: [Int: InstantPageNode] = [:]
|
|
|
|
var currentWebEmbedHeights: [Int : CGFloat] = [:]
|
|
var currentExpandedDetails: [Int : Bool]?
|
|
var currentDetailsItems: [InstantPageDetailsItem] = []
|
|
|
|
var currentAccessibilityAreas: [AccessibilityAreaNode] = []
|
|
|
|
private var isDeceleratingBecauseOfDragging = false
|
|
|
|
private let hiddenMediaDisposable = MetaDisposable()
|
|
private let resolveUrlDisposable = MetaDisposable()
|
|
private let loadWebpageDisposable = MetaDisposable()
|
|
|
|
private let loadProgress = ValuePromise<CGFloat>(1.0, ignoreRepeated: true)
|
|
private let loadProgressDisposable = MetaDisposable()
|
|
|
|
private let updateLayoutDisposable = MetaDisposable()
|
|
|
|
private var themeReferenceDate: Date?
|
|
|
|
var currentState: InstantPageStoredState {
|
|
var details: [InstantPageStoredDetailsState] = []
|
|
if let currentExpandedDetails = self.currentExpandedDetails {
|
|
for (index, expanded) in currentExpandedDetails {
|
|
details.append(InstantPageStoredDetailsState(index: Int32(clamping: index), expanded: expanded, details: []))
|
|
}
|
|
}
|
|
return InstantPageStoredState(contentOffset: Double(self.scrollNode.view.contentOffset.y), details: details)
|
|
}
|
|
|
|
init(controller: InstantPageController, context: AccountContext, settings: InstantPagePresentationSettings?, themeSettings: PresentationThemeSettings?, presentationTheme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, autoNightModeTriggered: Bool, statusBar: StatusBar, sourcePeerType: MediaAutoDownloadPeerType, getNavigationController: @escaping () -> NavigationController?, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, openPeer: @escaping (PeerId) -> Void, navigateBack: @escaping () -> Void) {
|
|
self.controller = controller
|
|
self.context = context
|
|
self.presentationTheme = presentationTheme
|
|
self.dateTimeFormat = dateTimeFormat
|
|
self.nameDisplayOrder = nameDisplayOrder
|
|
self.autoNightModeTriggered = autoNightModeTriggered
|
|
self.strings = strings
|
|
self.settings = settings
|
|
let themeReferenceDate = Date()
|
|
self.themeReferenceDate = themeReferenceDate
|
|
self.theme = settings.flatMap { settings in
|
|
return instantPageThemeForType(instantPageThemeTypeForSettingsAndTime(themeSettings: themeSettings, settings: settings, time: themeReferenceDate, forceDarkTheme: autoNightModeTriggered).0, settings: settings)
|
|
}
|
|
self.sourcePeerType = sourcePeerType
|
|
self.statusBar = statusBar
|
|
self.getNavigationController = getNavigationController
|
|
self.present = present
|
|
self.pushController = pushController
|
|
self.openPeer = openPeer
|
|
|
|
self.navigationBar = InstantPageNavigationBar(strings: strings)
|
|
self.scrollNode = ASScrollNode()
|
|
self.scrollNodeHeader = ASDisplayNode()
|
|
self.scrollNodeHeader.backgroundColor = .black
|
|
|
|
self.scrollNodeFooter = ASDisplayNode()
|
|
self.scrollNodeFooter.backgroundColor = .black
|
|
|
|
super.init()
|
|
|
|
self.setViewBlock({
|
|
return UITracingLayerView()
|
|
})
|
|
|
|
if let theme = self.theme {
|
|
self.backgroundColor = theme.pageBackgroundColor
|
|
self.scrollNodeFooter.backgroundColor = theme.panelBackgroundColor
|
|
}
|
|
self.addSubnode(self.scrollNode)
|
|
self.scrollNode.addSubnode(self.scrollNodeHeader)
|
|
self.scrollNode.addSubnode(self.scrollNodeFooter)
|
|
self.addSubnode(self.navigationBar)
|
|
self.scrollNode.view.delaysContentTouches = false
|
|
self.scrollNode.view.delegate = self
|
|
|
|
self.navigationBar.back = navigateBack
|
|
self.navigationBar.share = { [weak self] in
|
|
if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content {
|
|
let shareController = ShareController(context: context, subject: .url(content.url))
|
|
shareController.actionCompleted = { [weak self] in
|
|
if let strongSelf = self {
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
|
}
|
|
}
|
|
strongSelf.present(shareController, nil)
|
|
}
|
|
}
|
|
self.navigationBar.settings = { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.presentSettings()
|
|
}
|
|
}
|
|
self.navigationBar.scrollToTop = { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: -strongSelf.scrollNode.view.contentInset.top), animated: true)
|
|
}
|
|
}
|
|
|
|
self.loadProgressDisposable.set((self.loadProgress.get()
|
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
|
self?.navigationBar.setLoadProgress(value)
|
|
}))
|
|
}
|
|
|
|
deinit {
|
|
self.hiddenMediaDisposable.dispose()
|
|
self.resolveUrlDisposable.dispose()
|
|
self.loadWebpageDisposable.dispose()
|
|
self.loadProgressDisposable.dispose()
|
|
}
|
|
|
|
func update(settings: InstantPagePresentationSettings, themeSettings: PresentationThemeSettings?, strings: PresentationStrings) {
|
|
if self.settings != settings || self.strings !== strings {
|
|
let previousSettings = self.settings
|
|
var updateLayout = previousSettings == nil
|
|
|
|
if let previousSettings = previousSettings {
|
|
if previousSettings.themeType != settings.themeType {
|
|
self.themeReferenceDate = nil
|
|
}
|
|
}
|
|
|
|
self.settings = settings
|
|
self.themeSettings = themeSettings
|
|
let themeType = instantPageThemeTypeForSettingsAndTime(themeSettings: self.themeSettings, settings: settings, time: self.themeReferenceDate, forceDarkTheme: self.autoNightModeTriggered)
|
|
let theme = instantPageThemeForType(themeType.0, settings: settings)
|
|
self.theme = theme
|
|
self.strings = strings
|
|
|
|
self.settingsNode?.updateSettingsAndCurrentThemeType(settings: settings, type: themeType)
|
|
|
|
var animated = false
|
|
if let previousSettings = previousSettings {
|
|
if previousSettings.themeType != settings.themeType || previousSettings.autoNightMode != settings.autoNightMode {
|
|
updateLayout = true
|
|
animated = true
|
|
}
|
|
if previousSettings.fontSize != settings.fontSize || previousSettings.forceSerif != settings.forceSerif {
|
|
animated = false
|
|
updateLayout = true
|
|
}
|
|
}
|
|
|
|
self.backgroundColor = theme.pageBackgroundColor
|
|
|
|
if updateLayout {
|
|
if animated {
|
|
if let snapshotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) {
|
|
self.view.insertSubview(snapshotView, aboveSubview: self.scrollNode.view)
|
|
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak snapshotView] _ in
|
|
snapshotView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
self.updateLayout()
|
|
|
|
self.scrollNodeFooter.backgroundColor = theme.panelBackgroundColor
|
|
for (_, itemNode) in self.visibleItemsWithNodes {
|
|
itemNode.update(strings: strings, theme: theme)
|
|
}
|
|
|
|
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
|
|
self.updateNavigationBar()
|
|
|
|
self.recursivelyEnsureDisplaySynchronously(true)
|
|
|
|
if let layout = self.containerLayout {
|
|
self.containerLayoutUpdated(layout, navigationBarHeight: 0.0, transition: .immediate)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func tapActionAtPoint(_ point: CGPoint) -> TapLongTapOrDoubleTapGestureRecognizerAction {
|
|
if let currentLayout = self.currentLayout {
|
|
for item in currentLayout.items {
|
|
let frame = self.effectiveFrameForItem(item)
|
|
if frame.contains(point) {
|
|
if item is InstantPagePeerReferenceItem {
|
|
return .fail
|
|
} else if item is InstantPageAudioItem {
|
|
return .fail
|
|
} else if item is InstantPageArticleItem {
|
|
return .fail
|
|
} else if item is InstantPageFeedbackItem {
|
|
return .fail
|
|
} else if let item = item as? InstantPageDetailsItem {
|
|
for (_, itemNode) in self.visibleItemsWithNodes {
|
|
if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item {
|
|
return itemNode.tapActionAtPoint(point.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY))
|
|
}
|
|
}
|
|
}
|
|
if !(item is InstantPageImageItem || item is InstantPagePlayableVideoItem) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return .waitForSingleTap
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
|
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
|
}
|
|
|
|
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
|
recognizer.delaysTouchesBegan = false
|
|
recognizer.tapActionAtPoint = { [weak self] point in
|
|
if let strongSelf = self {
|
|
return strongSelf.tapActionAtPoint(point)
|
|
}
|
|
return .waitForSingleTap
|
|
}
|
|
recognizer.highlight = { [weak self] point in
|
|
if let strongSelf = self {
|
|
strongSelf.updateTouchesAtPoint(point)
|
|
}
|
|
}
|
|
self.scrollNode.view.addGestureRecognizer(recognizer)
|
|
}
|
|
|
|
func updateWebPage(_ webPage: TelegramMediaWebpage?, anchor: String?, state: InstantPageStoredState? = nil) {
|
|
if self.webPage != webPage {
|
|
if self.webPage != nil && self.currentLayout != nil {
|
|
if let snaphotView = self.scrollNode.view.snapshotView(afterScreenUpdates: false) {
|
|
self.scrollNode.view.superview?.insertSubview(snaphotView, aboveSubview: self.scrollNode.view)
|
|
snaphotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snaphotView] _ in
|
|
snaphotView?.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
|
|
self.setupScrollOffsetOnLayout = self.webPage == nil
|
|
self.webPage = webPage
|
|
if let anchor = anchor {
|
|
self.initialAnchor = anchor.removingPercentEncoding
|
|
} else if let state = state {
|
|
self.initialState = state
|
|
if !state.details.isEmpty {
|
|
var storedExpandedDetails: [Int: Bool] = [:]
|
|
for state in state.details {
|
|
storedExpandedDetails[Int(clamping: state.index)] = state.expanded
|
|
}
|
|
self.currentExpandedDetails = storedExpandedDetails
|
|
}
|
|
}
|
|
self.currentLayout = nil
|
|
self.updateLayout()
|
|
|
|
self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
|
|
if let containerLayout = self.containerLayout {
|
|
self.containerLayoutUpdated(containerLayout, navigationBarHeight: 0.0, transition: .immediate)
|
|
}
|
|
|
|
if let webPage = webPage, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, instantPage.isComplete {
|
|
self.loadProgress.set(1.0)
|
|
|
|
if let anchor = self.pendingAnchor {
|
|
self.pendingAnchor = nil
|
|
self.scrollToAnchor(anchor)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
self.containerLayout = layout
|
|
|
|
if let settingsDimNode = self.settingsDimNode {
|
|
transition.updateFrame(node: settingsDimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
|
}
|
|
|
|
if let settingsNode = self.settingsNode {
|
|
settingsNode.updateLayout(layout: layout, transition: transition)
|
|
transition.updateFrame(node: settingsNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
|
}
|
|
|
|
let maxBarHeight: CGFloat
|
|
if !layout.safeInsets.top.isZero {
|
|
if let statusBarHeight = layout.statusBarHeight, statusBarHeight > 34.0 {
|
|
maxBarHeight = statusBarHeight + 34.0
|
|
} else {
|
|
maxBarHeight = layout.safeInsets.top + 34.0
|
|
}
|
|
} else {
|
|
maxBarHeight = (layout.statusBarHeight ?? 0.0) + 44.0
|
|
}
|
|
|
|
let scrollInsetTop = maxBarHeight
|
|
|
|
let resetOffset = self.scrollNode.bounds.size.width.isZero || self.setupScrollOffsetOnLayout || !(self.initialAnchor ?? "").isEmpty
|
|
let widthUpdated = !self.scrollNode.bounds.size.width.isEqual(to: layout.size.width)
|
|
|
|
var shouldUpdateVisibleItems = false
|
|
if self.scrollNode.bounds.size != layout.size || !self.scrollNode.view.contentInset.top.isEqual(to: scrollInsetTop) {
|
|
self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
|
self.scrollNodeHeader.frame = CGRect(origin: CGPoint(x: 0.0, y: -2000.0), size: CGSize(width: layout.size.width, height: 2000.0))
|
|
self.scrollNode.view.contentInset = UIEdgeInsets(top: scrollInsetTop, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0)
|
|
if widthUpdated {
|
|
self.updateLayout()
|
|
}
|
|
shouldUpdateVisibleItems = true
|
|
self.updateNavigationBar()
|
|
}
|
|
var didSetScrollOffset = false
|
|
if resetOffset {
|
|
var contentOffset = CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top)
|
|
if let state = self.initialState {
|
|
didSetScrollOffset = true
|
|
contentOffset = CGPoint(x: 0.0, y: CGFloat(state.contentOffset))
|
|
}
|
|
else if let anchor = self.initialAnchor, !anchor.isEmpty {
|
|
self.initialAnchor = nil
|
|
if let items = self.currentLayout?.items {
|
|
didSetScrollOffset = true
|
|
if let (item, lineOffset, _, _) = self.findAnchorItem(anchor, items: items) {
|
|
contentOffset = CGPoint(x: 0.0, y: item.frame.minY + lineOffset - self.scrollNode.view.contentInset.top)
|
|
}
|
|
}
|
|
} else {
|
|
didSetScrollOffset = true
|
|
}
|
|
self.scrollNode.view.contentOffset = contentOffset
|
|
if didSetScrollOffset {
|
|
self.updateNavigationBar()
|
|
if self.currentLayout != nil {
|
|
self.setupScrollOffsetOnLayout = false
|
|
}
|
|
}
|
|
}
|
|
if shouldUpdateVisibleItems {
|
|
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
|
|
}
|
|
}
|
|
|
|
private func updateLayout() {
|
|
guard let containerLayout = self.containerLayout, let webPage = self.webPage, let theme = self.theme else {
|
|
return
|
|
}
|
|
|
|
let currentLayout = instantPageLayoutForWebPage(webPage, boundingWidth: containerLayout.size.width, safeInset: containerLayout.safeInsets.left, strings: self.strings, theme: theme, dateTimeFormat: self.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights)
|
|
|
|
for (_, tileNode) in self.visibleTiles {
|
|
tileNode.removeFromSupernode()
|
|
}
|
|
self.visibleTiles.removeAll()
|
|
|
|
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: containerLayout.size.width)
|
|
|
|
var currentDetailsItems: [InstantPageDetailsItem] = []
|
|
var currentLayoutItemsWithNodes: [InstantPageItem] = []
|
|
var distanceThresholdGroupCount: [Int : Int] = [:]
|
|
|
|
var expandedDetails: [Int : Bool] = [:]
|
|
|
|
var detailsIndex = -1
|
|
for item in currentLayout.items {
|
|
if item.wantsNode {
|
|
currentLayoutItemsWithNodes.append(item)
|
|
if let group = item.distanceThresholdGroup() {
|
|
let count: Int
|
|
if let currentCount = distanceThresholdGroupCount[Int(group)] {
|
|
count = currentCount
|
|
} else {
|
|
count = 0
|
|
}
|
|
distanceThresholdGroupCount[Int(group)] = count + 1
|
|
}
|
|
if let detailsItem = item as? InstantPageDetailsItem {
|
|
detailsIndex += 1
|
|
expandedDetails[detailsIndex] = detailsItem.initiallyExpanded
|
|
currentDetailsItems.append(detailsItem)
|
|
}
|
|
}
|
|
}
|
|
|
|
if var currentExpandedDetails = self.currentExpandedDetails {
|
|
for (index, expanded) in expandedDetails {
|
|
if currentExpandedDetails[index] == nil {
|
|
currentExpandedDetails[index] = expanded
|
|
}
|
|
}
|
|
self.currentExpandedDetails = currentExpandedDetails
|
|
} else {
|
|
self.currentExpandedDetails = expandedDetails
|
|
}
|
|
|
|
let accessibilityAreas = instantPageAccessibilityAreasFromLayout(currentLayout, boundingWidth: containerLayout.size.width)
|
|
|
|
self.currentLayout = currentLayout
|
|
self.currentLayoutTiles = currentLayoutTiles
|
|
self.currentLayoutItemsWithNodes = currentLayoutItemsWithNodes
|
|
self.currentDetailsItems = currentDetailsItems
|
|
self.distanceThresholdGroupCount = distanceThresholdGroupCount
|
|
|
|
for areaNode in self.currentAccessibilityAreas {
|
|
areaNode.removeFromSupernode()
|
|
}
|
|
for areaNode in accessibilityAreas {
|
|
self.scrollNode.addSubnode(areaNode)
|
|
}
|
|
self.currentAccessibilityAreas = accessibilityAreas
|
|
|
|
self.scrollNode.view.contentSize = currentLayout.contentSize
|
|
self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: currentLayout.contentSize.height), size: CGSize(width: containerLayout.size.width, height: 2000.0))
|
|
}
|
|
|
|
func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) {
|
|
guard let theme = self.theme else {
|
|
return
|
|
}
|
|
|
|
var visibleTileIndices = Set<Int>()
|
|
var visibleItemIndices = Set<Int>()
|
|
|
|
var topNode: ASDisplayNode?
|
|
let topTileNode = topNode
|
|
if let scrollSubnodes = self.scrollNode.subnodes {
|
|
for node in scrollSubnodes.reversed() {
|
|
if let node = node as? InstantPageTileNode {
|
|
topNode = node
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
var collapseOffset: CGFloat = 0.0
|
|
let transition: ContainedViewLayoutTransition
|
|
if animated {
|
|
transition = .animated(duration: 0.3, curve: .spring)
|
|
} else {
|
|
transition = .immediate
|
|
}
|
|
|
|
var itemIndex = -1
|
|
var embedIndex = -1
|
|
var detailsIndex = -1
|
|
|
|
var previousDetailsNode: InstantPageDetailsNode?
|
|
|
|
for item in self.currentLayoutItemsWithNodes {
|
|
itemIndex += 1
|
|
if item is InstantPageWebEmbedItem {
|
|
embedIndex += 1
|
|
}
|
|
if let imageItem = item as? InstantPageImageItem, imageItem.media.media is TelegramMediaWebpage {
|
|
embedIndex += 1
|
|
}
|
|
if item is InstantPageDetailsItem {
|
|
detailsIndex += 1
|
|
}
|
|
|
|
var itemThreshold: CGFloat = 0.0
|
|
if let group = item.distanceThresholdGroup() {
|
|
var count: Int = 0
|
|
if let currentCount = self.distanceThresholdGroupCount[group] {
|
|
count = currentCount
|
|
}
|
|
itemThreshold = item.distanceThresholdWithGroupCount(count)
|
|
}
|
|
|
|
var itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset)
|
|
var thresholdedItemFrame = itemFrame
|
|
thresholdedItemFrame.origin.y -= itemThreshold
|
|
thresholdedItemFrame.size.height += itemThreshold * 2.0
|
|
|
|
if let detailsItem = item as? InstantPageDetailsItem, let expanded = self.currentExpandedDetails?[detailsIndex] {
|
|
let height = expanded ? self.effectiveSizeForDetails(detailsItem).height : detailsItem.titleHeight
|
|
collapseOffset += itemFrame.height - height
|
|
itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: height))
|
|
}
|
|
|
|
if visibleBounds.intersects(thresholdedItemFrame) {
|
|
visibleItemIndices.insert(itemIndex)
|
|
|
|
var itemNode = self.visibleItemsWithNodes[itemIndex]
|
|
if let currentItemNode = itemNode {
|
|
if !item.matchesNode(currentItemNode) {
|
|
currentItemNode.removeFromSupernode()
|
|
self.visibleItemsWithNodes.removeValue(forKey: itemIndex)
|
|
itemNode = nil
|
|
}
|
|
}
|
|
|
|
if itemNode == nil {
|
|
let itemIndex = itemIndex
|
|
let embedIndex = embedIndex
|
|
let detailsIndex = detailsIndex
|
|
if let newNode = item.node(context: self.context, strings: self.strings, nameDisplayOrder: self.nameDisplayOrder, theme: theme, sourcePeerType: self.sourcePeerType, openMedia: { [weak self] media in
|
|
self?.openMedia(media)
|
|
}, longPressMedia: { [weak self] media in
|
|
self?.longPressMedia(media)
|
|
}, activatePinchPreview: { [weak self] sourceNode in
|
|
guard let strongSelf = self, let controller = strongSelf.controller else {
|
|
return
|
|
}
|
|
let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
|
|
guard let strongSelf = self else {
|
|
return CGRect()
|
|
}
|
|
|
|
let localRect = CGRect(origin: CGPoint(x: 0.0, y: strongSelf.navigationBar.frame.maxY), size: CGSize(width: strongSelf.bounds.width, height: strongSelf.bounds.height - strongSelf.navigationBar.frame.maxY))
|
|
return strongSelf.view.convert(localRect, to: nil)
|
|
})
|
|
controller.window?.presentInGlobalOverlay(pinchController)
|
|
}, pinchPreviewFinished: { [weak self] itemNode in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
for (_, listItemNode) in strongSelf.visibleItemsWithNodes {
|
|
if let listItemNode = listItemNode as? InstantPagePeerReferenceNode {
|
|
if listItemNode.frame.intersects(itemNode.frame) && listItemNode.frame.maxY <= itemNode.frame.maxY + 2.0 {
|
|
listItemNode.layer.animateAlpha(from: 0.0, to: listItemNode.alpha, duration: 0.25)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}, openPeer: { [weak self] peerId in
|
|
self?.openPeer(peerId)
|
|
}, openUrl: { [weak self] url in
|
|
self?.openUrl(url)
|
|
}, updateWebEmbedHeight: { [weak self] height in
|
|
self?.updateWebEmbedHeight(embedIndex, height)
|
|
}, updateDetailsExpanded: { [weak self] expanded in
|
|
self?.updateDetailsExpanded(detailsIndex, expanded)
|
|
}, currentExpandedDetails: self.currentExpandedDetails) {
|
|
newNode.frame = itemFrame
|
|
newNode.updateLayout(size: itemFrame.size, transition: transition)
|
|
if let topNode = topNode {
|
|
self.scrollNode.insertSubnode(newNode, aboveSubnode: topNode)
|
|
} else {
|
|
self.scrollNode.insertSubnode(newNode, at: 0)
|
|
}
|
|
topNode = newNode
|
|
self.visibleItemsWithNodes[itemIndex] = newNode
|
|
itemNode = newNode
|
|
|
|
if let itemNode = itemNode as? InstantPageDetailsNode {
|
|
itemNode.requestLayoutUpdate = { [weak self] animated in
|
|
if let strongSelf = self {
|
|
strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds, animated: animated)
|
|
}
|
|
}
|
|
|
|
if let previousDetailsNode = previousDetailsNode {
|
|
if itemNode.frame.minY - previousDetailsNode.frame.maxY < 1.0 {
|
|
itemNode.previousNode = previousDetailsNode
|
|
}
|
|
}
|
|
previousDetailsNode = itemNode
|
|
}
|
|
}
|
|
} else {
|
|
if let itemNode = itemNode, itemNode.frame != itemFrame {
|
|
transition.updateFrame(node: itemNode, frame: itemFrame)
|
|
itemNode.updateLayout(size: itemFrame.size, transition: transition)
|
|
}
|
|
}
|
|
|
|
if let itemNode = itemNode as? InstantPageDetailsNode {
|
|
itemNode.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemNode.frame.minX, dy: -itemNode.frame.minY), animated: animated)
|
|
}
|
|
}
|
|
}
|
|
|
|
topNode = topTileNode
|
|
|
|
var tileIndex = -1
|
|
for tile in self.currentLayoutTiles {
|
|
tileIndex += 1
|
|
|
|
let tileFrame = effectiveFrameForTile(tile)
|
|
var tileVisibleFrame = tileFrame
|
|
tileVisibleFrame.origin.y -= 400.0
|
|
tileVisibleFrame.size.height += 400.0 * 2.0
|
|
if tileVisibleFrame.intersects(visibleBounds) {
|
|
visibleTileIndices.insert(tileIndex)
|
|
|
|
if self.visibleTiles[tileIndex] == nil {
|
|
let tileNode = InstantPageTileNode(tile: tile, backgroundColor: theme.pageBackgroundColor)
|
|
tileNode.frame = tileFrame
|
|
if let topNode = topNode {
|
|
self.scrollNode.insertSubnode(tileNode, aboveSubnode: topNode)
|
|
} else {
|
|
self.scrollNode.insertSubnode(tileNode, at: 0)
|
|
}
|
|
topNode = tileNode
|
|
self.visibleTiles[tileIndex] = tileNode
|
|
} else {
|
|
if visibleTiles[tileIndex]!.frame != tileFrame {
|
|
transition.updateFrame(node: self.visibleTiles[tileIndex]!, frame: tileFrame)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let currentLayout = self.currentLayout {
|
|
let effectiveContentHeight = currentLayout.contentSize.height - collapseOffset
|
|
if effectiveContentHeight != self.scrollNode.view.contentSize.height {
|
|
transition.animateView {
|
|
self.scrollNode.view.contentSize = CGSize(width: currentLayout.contentSize.width, height: effectiveContentHeight)
|
|
}
|
|
let previousFrame = self.scrollNodeFooter.frame
|
|
self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: effectiveContentHeight), size: CGSize(width: previousFrame.width, height: 2000.0))
|
|
transition.animateFrame(node: self.scrollNodeFooter, from: previousFrame)
|
|
}
|
|
}
|
|
|
|
var removeTileIndices: [Int] = []
|
|
for (index, tileNode) in self.visibleTiles {
|
|
if !visibleTileIndices.contains(index) {
|
|
removeTileIndices.append(index)
|
|
tileNode.removeFromSupernode()
|
|
}
|
|
}
|
|
for index in removeTileIndices {
|
|
self.visibleTiles.removeValue(forKey: index)
|
|
}
|
|
|
|
var removeItemIndices: [Int] = []
|
|
for (index, itemNode) in self.visibleItemsWithNodes {
|
|
if !visibleItemIndices.contains(index) {
|
|
removeItemIndices.append(index)
|
|
itemNode.removeFromSupernode()
|
|
} else {
|
|
var itemFrame = itemNode.frame
|
|
let itemThreshold: CGFloat = 200.0
|
|
itemFrame.origin.y -= itemThreshold
|
|
itemFrame.size.height += itemThreshold * 2.0
|
|
itemNode.updateIsVisible(visibleBounds.intersects(itemFrame))
|
|
}
|
|
}
|
|
for index in removeItemIndices {
|
|
self.visibleItemsWithNodes.removeValue(forKey: index)
|
|
}
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
|
|
}
|
|
|
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
self.isDeceleratingBecauseOfDragging = decelerate
|
|
if !decelerate {
|
|
self.updateNavigationBar(forceState: true)
|
|
}
|
|
}
|
|
|
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
self.updateNavigationBar(forceState: true)
|
|
self.isDeceleratingBecauseOfDragging = false
|
|
}
|
|
|
|
func updateNavigationBar(forceState: Bool = false) {
|
|
guard let containerLayout = self.containerLayout else {
|
|
return
|
|
}
|
|
|
|
let bounds = self.scrollNode.view.bounds
|
|
|
|
let maxBarHeight: CGFloat
|
|
let minBarHeight: CGFloat
|
|
if !containerLayout.safeInsets.top.isZero {
|
|
if let statusBarHeight = containerLayout.statusBarHeight, statusBarHeight > 34.0 {
|
|
maxBarHeight = statusBarHeight + 44.0
|
|
} else {
|
|
maxBarHeight = containerLayout.safeInsets.top + 34.0
|
|
}
|
|
minBarHeight = containerLayout.safeInsets.top + 8.0
|
|
} else {
|
|
maxBarHeight = (containerLayout.statusBarHeight ?? 0.0) + 44.0
|
|
minBarHeight = 20.0
|
|
}
|
|
|
|
let transition: ContainedViewLayoutTransition = .immediate
|
|
var navigationBarFrame = self.navigationBar.frame
|
|
navigationBarFrame.size.width = bounds.size.width
|
|
if navigationBarFrame.size.height.isZero {
|
|
navigationBarFrame.size.height = maxBarHeight
|
|
}
|
|
|
|
navigationBarFrame.size.height = maxBarHeight
|
|
|
|
let transitionFactor = (navigationBarFrame.size.height - minBarHeight) / (maxBarHeight - minBarHeight)
|
|
|
|
if containerLayout.safeInsets.top.isZero {
|
|
let statusBarAlpha = min(1.0, max(0.0, transitionFactor))
|
|
transition.updateAlpha(node: self.statusBar, alpha: statusBarAlpha * statusBarAlpha)
|
|
self.statusBar.verticalOffset = navigationBarFrame.size.height - maxBarHeight
|
|
} else {
|
|
transition.updateAlpha(node: self.statusBar, alpha: 1.0)
|
|
self.statusBar.verticalOffset = 0.0
|
|
}
|
|
|
|
var title: String?
|
|
if let webPage = self.webPage, case let .Loaded(content) = webPage.content {
|
|
title = content.websiteName
|
|
}
|
|
|
|
transition.updateFrame(node: self.navigationBar, frame: navigationBarFrame)
|
|
self.navigationBar.updateLayout(size: navigationBarFrame.size, minHeight: minBarHeight, maxHeight: maxBarHeight, topInset: containerLayout.safeInsets.top, leftInset: containerLayout.safeInsets.left, rightInset: containerLayout.safeInsets.right, title: title, pageProgress: 0.0, transition: transition)
|
|
|
|
transition.animateView {
|
|
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: navigationBarFrame.size.height, left: 0.0, bottom: containerLayout.intrinsicInsets.bottom, right: 0.0)
|
|
}
|
|
}
|
|
|
|
private func updateTouchesAtPoint(_ location: CGPoint?) {
|
|
var rects: [CGRect]?
|
|
if let location = location, let currentLayout = self.currentLayout {
|
|
for item in currentLayout.items {
|
|
let itemFrame = self.effectiveFrameForItem(item)
|
|
if itemFrame.contains(location) {
|
|
var contentOffset = CGPoint()
|
|
if let item = item as? InstantPageScrollableItem {
|
|
contentOffset = self.scrollableContentOffset(item: item)
|
|
}
|
|
var itemRects = item.linkSelectionRects(at: location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY))
|
|
|
|
for i in 0 ..< itemRects.count {
|
|
itemRects[i] = itemRects[i].offsetBy(dx: itemFrame.minX - contentOffset.x, dy: itemFrame.minY).insetBy(dx: -2.0, dy: -2.0)
|
|
}
|
|
if !itemRects.isEmpty {
|
|
rects = itemRects
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let rects = rects {
|
|
let linkHighlightingNode: LinkHighlightingNode
|
|
if let current = self.linkHighlightingNode {
|
|
linkHighlightingNode = current
|
|
} else {
|
|
let highlightColor = self.theme?.linkHighlightColor ?? UIColor(rgb: 0x007aff).withAlphaComponent(0.4)
|
|
linkHighlightingNode = LinkHighlightingNode(color: highlightColor)
|
|
linkHighlightingNode.isUserInteractionEnabled = false
|
|
self.linkHighlightingNode = linkHighlightingNode
|
|
self.scrollNode.addSubnode(linkHighlightingNode)
|
|
}
|
|
linkHighlightingNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)
|
|
linkHighlightingNode.updateRects(rects)
|
|
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
|
self.linkHighlightingNode = nil
|
|
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
|
linkHighlightingNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
}
|
|
|
|
private func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint {
|
|
var contentOffset = CGPoint()
|
|
for (_, itemNode) in self.visibleItemsWithNodes {
|
|
if let itemNode = itemNode as? InstantPageScrollableNode, itemNode.item === item {
|
|
contentOffset = itemNode.contentOffset
|
|
break
|
|
}
|
|
}
|
|
return contentOffset
|
|
}
|
|
|
|
private func nodeForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsNode? {
|
|
for (_, itemNode) in self.visibleItemsWithNodes {
|
|
if let detailsNode = itemNode as? InstantPageDetailsNode, detailsNode.item === item {
|
|
return detailsNode
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize {
|
|
if let node = nodeForDetailsItem(item) {
|
|
return CGSize(width: item.frame.width, height: node.effectiveContentSize.height + item.titleHeight)
|
|
} else {
|
|
return item.frame.size
|
|
}
|
|
}
|
|
|
|
private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect {
|
|
let layoutOrigin = tile.frame.origin
|
|
var origin = layoutOrigin
|
|
for item in self.currentDetailsItems {
|
|
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
|
|
if layoutOrigin.y >= item.frame.maxY {
|
|
let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
|
|
origin.y += height - item.frame.height
|
|
}
|
|
}
|
|
return CGRect(origin: origin, size: tile.frame.size)
|
|
}
|
|
|
|
private func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect {
|
|
let layoutOrigin = item.frame.origin
|
|
var origin = layoutOrigin
|
|
|
|
for item in self.currentDetailsItems {
|
|
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
|
|
if layoutOrigin.y >= item.frame.maxY {
|
|
let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
|
|
origin.y += height - item.frame.height
|
|
}
|
|
}
|
|
|
|
if let item = item as? InstantPageDetailsItem {
|
|
let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded
|
|
let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight
|
|
return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: height))
|
|
} else {
|
|
return CGRect(origin: origin, size: item.frame.size)
|
|
}
|
|
}
|
|
|
|
private func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? {
|
|
if let currentLayout = self.currentLayout {
|
|
for item in currentLayout.items {
|
|
let itemFrame = self.effectiveFrameForItem(item)
|
|
if itemFrame.contains(location) {
|
|
if let item = item as? InstantPageTextItem, item.selectable {
|
|
return (item, CGPoint(x: itemFrame.minX - item.frame.minX, y: itemFrame.minY - item.frame.minY))
|
|
} else if let item = item as? InstantPageScrollableItem {
|
|
let contentOffset = scrollableContentOffset(item: item)
|
|
if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) {
|
|
return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x - contentOffset.x, dy: parentOffset.y))
|
|
}
|
|
} else if let item = item as? InstantPageDetailsItem {
|
|
for (_, itemNode) in self.visibleItemsWithNodes {
|
|
if let itemNode = itemNode as? InstantPageDetailsNode, itemNode.item === item {
|
|
if let (textItem, parentOffset) = itemNode.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX, dy: -itemFrame.minY)) {
|
|
return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func urlForTapLocation(_ location: CGPoint) -> InstantPageUrlItem? {
|
|
if let (item, parentOffset) = self.textItemAtLocation(location) {
|
|
return item.urlAttribute(at: location.offsetBy(dx: -item.frame.minX - parentOffset.x, dy: -item.frame.minY - parentOffset.y))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func longPressMedia(_ media: InstantPageMedia) {
|
|
let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
|
if let strongSelf = self, let image = media.media as? TelegramMediaImage {
|
|
let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
|
let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .standalone(media: media)).start()
|
|
}
|
|
}), ContextMenuAction(content: .text(title: self.strings.Conversation_LinkDialogSave, accessibilityLabel: self.strings.Conversation_LinkDialogSave), action: { [weak self] in
|
|
if let strongSelf = self, let image = media.media as? TelegramMediaImage {
|
|
let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
|
|
let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .standalone(media: media)).start()
|
|
}
|
|
}), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
|
|
if let strongSelf = self, let webPage = strongSelf.webPage, let image = media.media as? TelegramMediaImage {
|
|
strongSelf.present(ShareController(context: strongSelf.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil)
|
|
}
|
|
})], catchTapsOutside: true)
|
|
self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
|
if let strongSelf = self {
|
|
for (_, itemNode) in strongSelf.visibleItemsWithNodes {
|
|
if let (node, _, _) = itemNode.transitionNode(media: media) {
|
|
return (strongSelf.scrollNode, node.convert(node.bounds, to: strongSelf.scrollNode), strongSelf, strongSelf.bounds)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}))
|
|
}
|
|
|
|
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .ended:
|
|
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
|
switch gesture {
|
|
case .tap:
|
|
if let url = self.urlForTapLocation(location) {
|
|
self.openUrl(url)
|
|
}
|
|
case .longTap:
|
|
if let theme = self.theme, let url = self.urlForTapLocation(location) {
|
|
let canOpenIn = availableOpenInOptions(context: self.context, item: .url(url: url.url)).count > 1
|
|
let openText = canOpenIn ? self.strings.Conversation_FileOpenIn : self.strings.Conversation_LinkDialogOpen
|
|
let actionSheet = ActionSheetController(instantPageTheme: theme)
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
|
ActionSheetTextItem(title: url.url),
|
|
ActionSheetButtonItem(title: openText, color: .accent, action: { [weak self, weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
if let strongSelf = self {
|
|
if canOpenIn {
|
|
strongSelf.openUrlIn(url)
|
|
} else {
|
|
strongSelf.openUrl(url)
|
|
}
|
|
}
|
|
}),
|
|
ActionSheetButtonItem(title: self.strings.ShareMenu_CopyShareLink, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
UIPasteboard.general.string = url.url
|
|
}),
|
|
ActionSheetButtonItem(title: self.strings.Conversation_AddToReadingList, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
if let link = URL(string: url.url) {
|
|
let _ = try? SSReadingList.default()?.addItem(with: link, title: nil, previewText: nil)
|
|
}
|
|
})
|
|
]), ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: self.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])])
|
|
self.present(actionSheet, nil)
|
|
} else if let (item, parentOffset) = self.textItemAtLocation(location) {
|
|
let textFrame = item.frame
|
|
var itemRects = item.lineRects()
|
|
for i in 0 ..< itemRects.count {
|
|
itemRects[i] = itemRects[i].offsetBy(dx: parentOffset.x + textFrame.minX, dy: parentOffset.y + textFrame.minY).insetBy(dx: -2.0, dy: -2.0)
|
|
}
|
|
self.updateTextSelectionRects(itemRects, text: item.plainText())
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private func updateTextSelectionRects(_ rects: [CGRect], text: String?) {
|
|
if let text = text, !rects.isEmpty {
|
|
let textSelectionNode: LinkHighlightingNode
|
|
if let current = self.textSelectionNode {
|
|
textSelectionNode = current
|
|
} else {
|
|
textSelectionNode = LinkHighlightingNode(color: UIColor.lightGray.withAlphaComponent(0.4))
|
|
textSelectionNode.isUserInteractionEnabled = false
|
|
self.textSelectionNode = textSelectionNode
|
|
self.scrollNode.addSubnode(textSelectionNode)
|
|
}
|
|
textSelectionNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.bounds.size)
|
|
textSelectionNode.updateRects(rects)
|
|
|
|
var coveringRect = rects[0]
|
|
for i in 1 ..< rects.count {
|
|
coveringRect = coveringRect.union(rects[i])
|
|
}
|
|
|
|
let context = self.context
|
|
let strings = self.strings
|
|
let _ = (context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.translationSettings])
|
|
|> take(1)
|
|
|> deliverOnMainQueue).start(next: { [weak self] sharedData in
|
|
let translationSettings: TranslationSettings
|
|
if let current = sharedData.entries[ApplicationSpecificSharedDataKeys.translationSettings]?.get(TranslationSettings.self) {
|
|
translationSettings = current
|
|
} else {
|
|
translationSettings = TranslationSettings.defaultSettings
|
|
}
|
|
|
|
var actions: [ContextMenuAction] = [ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuCopy, accessibilityLabel: strings.Conversation_ContextMenuCopy), action: { [weak self] in
|
|
UIPasteboard.general.string = text
|
|
|
|
if let strongSelf = self {
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
|
}
|
|
}), ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuShare, accessibilityLabel: strings.Conversation_ContextMenuShare), action: { [weak self] in
|
|
if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content {
|
|
strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil)
|
|
}
|
|
})]
|
|
|
|
let (canTranslate, language) = canTranslateText(context: context, text: text, showTranslate: translationSettings.showTranslate, ignoredLanguages: translationSettings.ignoredLanguages)
|
|
if canTranslate {
|
|
actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: {
|
|
translateText(context: context, text: text, fromLang: language)
|
|
}))
|
|
}
|
|
|
|
let controller = ContextMenuController(actions: actions)
|
|
controller.dismissed = { [weak self] in
|
|
self?.updateTextSelectionRects([], text: nil)
|
|
}
|
|
self?.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
|
|
if let strongSelf = self {
|
|
return (strongSelf.scrollNode, coveringRect.insetBy(dx: -3.0, dy: -3.0), strongSelf, strongSelf.bounds)
|
|
} else {
|
|
return nil
|
|
}
|
|
}))
|
|
})
|
|
|
|
textSelectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
|
} else if let textSelectionNode = self.textSelectionNode {
|
|
self.textSelectionNode = nil
|
|
textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in
|
|
textSelectionNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
}
|
|
|
|
private func findAnchorItem(_ anchor: String, items: [InstantPageItem]) -> (InstantPageItem, CGFloat, Bool, [InstantPageDetailsItem])? {
|
|
for item in items {
|
|
if let item = item as? InstantPageAnchorItem, item.anchor == anchor {
|
|
return (item, -10.0, false, [])
|
|
} else if let item = item as? InstantPageTextItem {
|
|
if let (lineIndex, empty) = item.anchors[anchor] {
|
|
return (item, item.lines[lineIndex].frame.minY - 10.0, !empty, [])
|
|
}
|
|
}
|
|
else if let item = item as? InstantPageTableItem {
|
|
if let (offset, empty) = item.anchors[anchor] {
|
|
return (item, offset - 10.0, !empty, [])
|
|
}
|
|
}
|
|
else if let item = item as? InstantPageDetailsItem {
|
|
if let (foundItem, offset, reference, detailsItems) = self.findAnchorItem(anchor, items: item.items) {
|
|
var detailsItems = detailsItems
|
|
detailsItems.insert(item, at: 0)
|
|
return (foundItem, offset, reference, detailsItems)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func presentReferenceView(item: InstantPageTextItem, referenceAnchor: String) {
|
|
guard let theme = self.theme, let webPage = self.webPage else {
|
|
return
|
|
}
|
|
|
|
var targetAnchor: InstantPageTextAnchorItem?
|
|
for (name, (line, _)) in item.anchors {
|
|
if name == referenceAnchor {
|
|
let anchors = item.lines[line].anchorItems
|
|
for anchor in anchors {
|
|
if anchor.name == referenceAnchor {
|
|
targetAnchor = anchor
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
guard let anchorText = targetAnchor?.anchorText else {
|
|
return
|
|
}
|
|
|
|
let controller = InstantPageReferenceController(context: self.context, sourcePeerType: self.sourcePeerType, theme: theme, webPage: webPage, anchorText: anchorText, openUrl: { [weak self] url in
|
|
self?.openUrl(url)
|
|
}, openUrlIn: { [weak self] url in
|
|
self?.openUrlIn(url)
|
|
}, present: { [weak self] c, a in
|
|
self?.present(c, a)
|
|
})
|
|
self.present(controller, nil)
|
|
}
|
|
|
|
private func scrollToAnchor(_ anchor: String) {
|
|
guard let items = self.currentLayout?.items else {
|
|
return
|
|
}
|
|
|
|
if !anchor.isEmpty {
|
|
if let (item, lineOffset, reference, detailsItems) = findAnchorItem(String(anchor), items: items) {
|
|
if let item = item as? InstantPageTextItem, reference {
|
|
self.presentReferenceView(item: item, referenceAnchor: anchor)
|
|
} else {
|
|
var previousDetailsNode: InstantPageDetailsNode?
|
|
var containerOffset: CGFloat = 0.0
|
|
for detailsItem in detailsItems {
|
|
if let previousNode = previousDetailsNode {
|
|
previousNode.contentNode.updateDetailsExpanded(detailsItem.index, true, animated: false)
|
|
let frame = previousNode.effectiveFrameForItem(detailsItem)
|
|
containerOffset += frame.minY
|
|
|
|
previousDetailsNode = previousNode.contentNode.nodeForDetailsItem(detailsItem)
|
|
previousDetailsNode?.setExpanded(true, animated: false)
|
|
} else {
|
|
self.updateDetailsExpanded(detailsItem.index, true, animated: false)
|
|
let frame = self.effectiveFrameForItem(detailsItem)
|
|
containerOffset += frame.minY
|
|
|
|
previousDetailsNode = self.nodeForDetailsItem(detailsItem)
|
|
previousDetailsNode?.setExpanded(true, animated: false)
|
|
}
|
|
}
|
|
|
|
let frame: CGRect
|
|
if let previousDetailsNode = previousDetailsNode {
|
|
frame = previousDetailsNode.effectiveFrameForItem(item)
|
|
} else {
|
|
frame = self.effectiveFrameForItem(item)
|
|
}
|
|
|
|
var targetY = min(containerOffset + frame.minY + lineOffset, self.scrollNode.view.contentSize.height - self.scrollNode.frame.height)
|
|
if targetY < self.scrollNode.view.contentOffset.y {
|
|
targetY -= self.scrollNode.view.contentInset.top
|
|
} else {
|
|
targetY -= self.containerLayout?.statusBarHeight ?? 20.0
|
|
}
|
|
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: targetY), animated: true)
|
|
}
|
|
} else if let webPage = self.webPage, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, !instantPage.isComplete {
|
|
self.loadProgress.set(0.5)
|
|
self.pendingAnchor = anchor
|
|
}
|
|
} else {
|
|
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: -self.scrollNode.view.contentInset.top), animated: true)
|
|
}
|
|
}
|
|
|
|
private func openUrl(_ url: InstantPageUrlItem) {
|
|
var baseUrl = url.url
|
|
var anchor: String?
|
|
if let anchorRange = url.url.range(of: "#") {
|
|
anchor = String(baseUrl[anchorRange.upperBound...]).removingPercentEncoding
|
|
baseUrl = String(baseUrl[..<anchorRange.lowerBound])
|
|
}
|
|
|
|
if let webPage = self.webPage, case let .Loaded(content) = webPage.content, let page = content.instantPage, page.url == baseUrl, let anchor = anchor {
|
|
self.scrollToAnchor(anchor)
|
|
return
|
|
}
|
|
|
|
self.loadProgress.set(0.0)
|
|
self.loadProgress.set(0.02)
|
|
|
|
self.loadWebpageDisposable.set(nil)
|
|
self.resolveUrlDisposable.set((self.context.sharedContext.resolveUrl(context: self.context, peerId: nil, url: url.url, skipUrlAuth: true)
|
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
|
if let strongSelf = self {
|
|
strongSelf.loadProgress.set(0.07)
|
|
switch result {
|
|
case let .externalUrl(externalUrl):
|
|
if let webpageId = url.webpageId {
|
|
var anchor: String?
|
|
if let anchorRange = externalUrl.range(of: "#") {
|
|
anchor = String(externalUrl[anchorRange.upperBound...])
|
|
}
|
|
strongSelf.loadWebpageDisposable.set((webpagePreviewWithProgress(account: strongSelf.context.account, url: externalUrl, webpageId: webpageId)
|
|
|> deliverOnMainQueue).start(next: { result in
|
|
if let strongSelf = self {
|
|
switch result {
|
|
case let .result(webpage):
|
|
if let webpage = webpage, case .Loaded = webpage.content {
|
|
strongSelf.loadProgress.set(1.0)
|
|
strongSelf.pushController(InstantPageController(context: strongSelf.context, webPage: webpage, sourcePeerType: strongSelf.sourcePeerType, anchor: anchor))
|
|
}
|
|
break
|
|
case let .progress(progress):
|
|
strongSelf.loadProgress.set(CGFloat(0.07 + progress * (1.0 - 0.07)))
|
|
}
|
|
}
|
|
}))
|
|
} else {
|
|
strongSelf.loadProgress.set(1.0)
|
|
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: externalUrl, forceExternal: false, presentationData: strongSelf.context.sharedContext.currentPresentationData.with { $0 }, navigationController: strongSelf.getNavigationController(), dismissInput: {
|
|
self?.view.endEditing(true)
|
|
})
|
|
}
|
|
default:
|
|
strongSelf.loadProgress.set(1.0)
|
|
strongSelf.context.sharedContext.openResolvedUrl(result, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.getNavigationController(), openPeer: { peerId, navigation in
|
|
switch navigation {
|
|
case let .chat(_, subject, peekData):
|
|
if let navigationController = strongSelf.getNavigationController() {
|
|
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), subject: subject, peekData: peekData))
|
|
}
|
|
case let .withBotStartPayload(botStart):
|
|
if let navigationController = strongSelf.getNavigationController() {
|
|
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peerId), botStart: botStart, keepStack: .always))
|
|
}
|
|
case .info:
|
|
let _ = (strongSelf.context.account.postbox.loadedPeerWithId(peerId)
|
|
|> deliverOnMainQueue).start(next: { peer in
|
|
if let strongSelf = self {
|
|
if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
|
|
strongSelf.getNavigationController()?.pushViewController(controller)
|
|
}
|
|
}
|
|
})
|
|
default:
|
|
break
|
|
}
|
|
}, sendFile: nil,
|
|
sendSticker: nil,
|
|
requestMessageActionUrlAuth: nil,
|
|
joinVoiceChat: nil,
|
|
present: { c, a in
|
|
self?.present(c, a)
|
|
}, dismissInput: {
|
|
self?.view.endEditing(true)
|
|
}, contentContext: nil)
|
|
}
|
|
}
|
|
}))
|
|
}
|
|
|
|
private func openUrlIn(_ url: InstantPageUrlItem) {
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in
|
|
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {
|
|
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
|
|
}
|
|
})
|
|
self.present(actionSheet, nil)
|
|
}
|
|
|
|
private func mediasFromItems(_ items: [InstantPageItem]) -> [InstantPageMedia] {
|
|
var medias: [InstantPageMedia] = []
|
|
for item in items {
|
|
if let detailsItem = item as? InstantPageDetailsItem {
|
|
medias.append(contentsOf: mediasFromItems(detailsItem.items))
|
|
} else {
|
|
medias.append(contentsOf: item.medias)
|
|
}
|
|
}
|
|
return medias
|
|
}
|
|
|
|
private func openMedia(_ media: InstantPageMedia) {
|
|
guard let items = self.currentLayout?.items, let webPage = self.webPage else {
|
|
return
|
|
}
|
|
|
|
if let map = media.media as? TelegramMediaMap {
|
|
let controllerParams = LocationViewParams(sendLiveLocation: { _ in
|
|
}, stopLiveLocation: { _ in
|
|
}, openUrl: { _ in }, openPeer: { _ in
|
|
}, showAll: false)
|
|
|
|
let peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: "", lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [])
|
|
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peer, text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [])
|
|
|
|
let controller = LocationViewController(context: self.context, subject: message, params: controllerParams)
|
|
self.pushController(controller)
|
|
return
|
|
}
|
|
|
|
if let file = media.media as? TelegramMediaFile, (file.isVoice || file.isMusic) {
|
|
var medias: [InstantPageMedia] = []
|
|
var initialIndex = 0
|
|
for item in items {
|
|
for itemMedia in item.medias {
|
|
if let itemFile = itemMedia.media as? TelegramMediaFile, (itemFile.isVoice || itemFile.isMusic) {
|
|
if itemMedia.index == media.index {
|
|
initialIndex = medias.count
|
|
}
|
|
medias.append(itemMedia)
|
|
}
|
|
}
|
|
}
|
|
self.context.sharedContext.mediaManager.setPlaylist((self.context.account, InstantPageMediaPlaylist(webPage: webPage, items: medias, initialItemIndex: initialIndex)), type: file.isVoice ? .voice : .music, control: .playback(.play))
|
|
return
|
|
}
|
|
|
|
var fromPlayingVideo = false
|
|
|
|
var entries: [InstantPageGalleryEntry] = []
|
|
if media.media is TelegramMediaWebpage {
|
|
entries.append(InstantPageGalleryEntry(index: 0, pageId: webPage.webpageId, media: media, caption: nil, credit: nil, location: nil))
|
|
} else if let file = media.media as? TelegramMediaFile, file.isAnimated {
|
|
fromPlayingVideo = true
|
|
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil))
|
|
} else {
|
|
fromPlayingVideo = true
|
|
var medias: [InstantPageMedia] = mediasFromItems(items)
|
|
medias = medias.filter {
|
|
return $0.media is TelegramMediaImage || $0.media is TelegramMediaFile
|
|
}
|
|
|
|
for media in medias {
|
|
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: InstantPageGalleryEntryLocation(position: Int32(entries.count), totalCount: Int32(medias.count))))
|
|
}
|
|
}
|
|
|
|
var centralIndex: Int?
|
|
for i in 0 ..< entries.count {
|
|
if entries[i].media == media {
|
|
centralIndex = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if let centralIndex = centralIndex {
|
|
let controller = InstantPageGalleryController(context: self.context, webPage: webPage, entries: entries, centralIndex: centralIndex, fromPlayingVideo: fromPlayingVideo, replaceRootController: { _, _ in
|
|
}, baseNavigationController: self.getNavigationController())
|
|
self.hiddenMediaDisposable.set((controller.hiddenMedia |> deliverOnMainQueue).start(next: { [weak self] entry in
|
|
if let strongSelf = self {
|
|
for (_, itemNode) in strongSelf.visibleItemsWithNodes {
|
|
itemNode.updateHiddenMedia(media: entry?.media)
|
|
}
|
|
}
|
|
}))
|
|
controller.openUrl = { [weak self] url in
|
|
self?.openUrl(url)
|
|
}
|
|
self.present(controller, InstantPageGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in
|
|
if let strongSelf = self {
|
|
for (_, itemNode) in strongSelf.visibleItemsWithNodes {
|
|
if let transitionNode = itemNode.transitionNode(media: entry.media) {
|
|
return GalleryTransitionArguments(transitionNode: transitionNode, addToTransitionSurface: { view in
|
|
if let strongSelf = self {
|
|
strongSelf.scrollNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.scrollNode.view)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}))
|
|
}
|
|
}
|
|
|
|
private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) {
|
|
let currentHeight = self.currentWebEmbedHeights[index]
|
|
if height != currentHeight {
|
|
if let currentHeight = currentHeight, currentHeight > height {
|
|
return
|
|
}
|
|
self.currentWebEmbedHeights[index] = height
|
|
|
|
let signal: Signal<Void, NoError> = (.complete() |> delay(0.08, queue: Queue.mainQueue()))
|
|
self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.updateLayout()
|
|
strongSelf.updateVisibleItems(visibleBounds: strongSelf.scrollNode.view.bounds)
|
|
}
|
|
}))
|
|
}
|
|
}
|
|
|
|
private func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true) {
|
|
if var currentExpandedDetails = self.currentExpandedDetails {
|
|
currentExpandedDetails[index] = expanded
|
|
self.currentExpandedDetails = currentExpandedDetails
|
|
}
|
|
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds, animated: animated)
|
|
}
|
|
|
|
private func presentSettings() {
|
|
guard let settings = self.settings, let containerLayout = self.containerLayout else {
|
|
return
|
|
}
|
|
if self.settingsNode == nil {
|
|
let settingsNode = InstantPageSettingsNode(strings: self.strings, settings: settings, currentThemeType: instantPageThemeTypeForSettingsAndTime(themeSettings: self.themeSettings, settings: settings, time: self.themeReferenceDate, forceDarkTheme: self.autoNightModeTriggered), applySettings: { [weak self] settings in
|
|
if let strongSelf = self {
|
|
strongSelf.update(settings: settings, themeSettings: strongSelf.themeSettings, strings: strongSelf.strings)
|
|
let _ = updateInstantPagePresentationSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { _ in
|
|
return settings
|
|
}).start()
|
|
}
|
|
}, openInSafari: { [weak self] in
|
|
if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content {
|
|
strongSelf.context.sharedContext.applicationBindings.openUrl(content.url)
|
|
}
|
|
})
|
|
self.addSubnode(settingsNode)
|
|
self.settingsNode = settingsNode
|
|
|
|
let settingsDimNode = ASDisplayNode()
|
|
settingsDimNode.backgroundColor = UIColor(rgb: 0, alpha: 0.1)
|
|
settingsDimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(settingsDimTapped(_:))))
|
|
self.insertSubnode(settingsDimNode, belowSubnode: self.navigationBar)
|
|
self.settingsDimNode = settingsDimNode
|
|
|
|
settingsDimNode.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
|
|
|
settingsNode.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
|
settingsNode.updateLayout(layout: containerLayout, transition: .immediate)
|
|
settingsNode.animateIn()
|
|
settingsDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
|
|
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: .easeInOut)
|
|
self.navigationBar.updateDimmed(true, transition: transition)
|
|
transition.updateAlpha(node: self.statusBar, alpha: 0.5)
|
|
}
|
|
}
|
|
|
|
@objc func settingsDimTapped(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
if let settingsNode = self.settingsNode {
|
|
self.settingsNode = nil
|
|
settingsNode.animateOut(completion: { [weak settingsNode] in
|
|
settingsNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
|
|
if let settingsDimNode = self.settingsDimNode {
|
|
self.settingsDimNode = nil
|
|
settingsDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak settingsDimNode] _ in
|
|
settingsDimNode?.removeFromSupernode()
|
|
})
|
|
}
|
|
|
|
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
|
|
self.navigationBar.updateDimmed(false, transition: transition)
|
|
transition.updateAlpha(node: self.statusBar, alpha: 1.0)
|
|
}
|
|
}
|
|
}
|