Swiftgram/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift
2024-07-02 21:03:22 +04:00

1041 lines
51 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import TelegramCore
import Postbox
import SwiftSignalKit
import Display
import ComponentFlow
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import AppBundle
import InstantPageUI
import UndoUI
import TranslateUI
import ContextUI
import Pasteboard
import SaveToCameraRoll
import ShareController
private final class InstantPageContainerNode: ASDisplayNode, UIScrollViewDelegate {
private let context: AccountContext
private let webPage: TelegramMediaWebpage
private let presentationData: PresentationData
private let theme: InstantPageTheme
private let sourceLocation: InstantPageSourceLocation
private var initialAnchor: String?
private var pendingAnchor: String?
private var initialState: InstantPageStoredState?
fileprivate let scrollNode: ASScrollNode
private let scrollNodeFooter: ASDisplayNode
private var linkHighlightingNode: LinkHighlightingNode?
private var textSelectionNode: LinkHighlightingNode?
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] = []
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
var openMedia: (InstantPageMedia) -> Void = { _ in }
var longPressMedia: (InstantPageMedia) -> Void = { _ in }
var openPeer: (EnginePeer) -> Void = { _ in }
var openUrl: (InstantPageUrlItem) -> Void = { _ in }
var activatePinchPreview: ((PinchSourceContainerNode) -> Void)?
var pinchPreviewFinished: ((InstantPageNode) -> Void)?
var present: (ViewController, Any?) -> Void = { _, _ in }
private let updateLayoutDisposable = MetaDisposable()
private var containerLayout: (size: CGSize, insets: UIEdgeInsets)?
init(context: AccountContext, webPage: TelegramMediaWebpage, sourceLocation: InstantPageSourceLocation) {
self.context = context
self.webPage = webPage
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
self.theme = instantPageThemeForType(.light, settings: .defaultSettings)
self.sourceLocation = sourceLocation
self.scrollNode = ASScrollNode()
self.scrollNode.backgroundColor = self.theme.pageBackgroundColor
self.scrollNodeFooter = ASDisplayNode()
self.scrollNodeFooter.backgroundColor = self.theme.panelBackgroundColor
super.init()
self.addSubnode(self.scrollNode)
self.scrollNode.addSubnode(self.scrollNodeFooter)
self.scrollNode.view.delaysContentTouches = false
self.scrollNode.view.delegate = self
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)
}
deinit {
self.updateLayoutDisposable.dispose()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
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
}
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
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()
})
}
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
self.containerLayout = (size, insets)
var scrollInsets = insets
scrollInsets.top = 0.0
if self.scrollNode.view.contentInset != insets {
self.scrollNode.view.contentInset = scrollInsets
self.scrollNode.view.scrollIndicatorInsets = scrollInsets
}
self.scrollNode.frame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top))
if self.currentLayout?.contentSize.width != size.width {
self.updatePageLayout()
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
}
}
private func updatePageLayout() {
guard let (size, insets) = self.containerLayout else {
return
}
let currentLayout = instantPageLayoutForWebPage(webPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights)
for (_, tileNode) in self.visibleTiles {
tileNode.removeFromSupernode()
}
self.visibleTiles.removeAll()
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: 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: 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: size.width, height: 2000.0))
}
func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) {
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._asMedia() 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.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, theme: self.theme, sourceLocation: self.sourceLocation, openMedia: { [weak self] media in
self?.openMedia(media)
}, longPressMedia: { [weak self] media in
self?.longPressMedia(media)
}, activatePinchPreview: { [weak self] sourceNode in
let _ = self
// 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
let _ = self
// 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)
}
}
private struct ScrollingOffsetState: Equatable {
var value: CGFloat
var isDraggingOrDecelerating: Bool
}
private var previousScrollingOffset: ScrollingOffsetState?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
self.updateScrollingOffset(isReset: false, transition: .immediate)
}
private func snapScrollingOffsetToInsets() {
let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))
self.updateScrollingOffset(isReset: false, transition: transition)
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.snapScrollingOffsetToInsets()
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.snapScrollingOffsetToInsets()
}
private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) {
let scrollView = self.scrollNode.view
let isInteracting = scrollView.isDragging || scrollView.isDecelerating
if let previousScrollingOffsetValue = self.previousScrollingOffset {
let currentBounds = scrollView.bounds
let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)
let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY)
let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value
self.onScrollingUpdate(ContentScrollingUpdate(
relativeOffset: relativeOffset,
absoluteOffsetToTopEdge: offsetToTopEdge,
absoluteOffsetToBottomEdge: offsetToBottomEdge,
isReset: isReset,
isInteracting: isInteracting,
transition: transition
))
}
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting)
}
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 = makeContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in
if let self, let image = media.media._asMedia() 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: self.context, postbox: self.context.account.postbox, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
}
}), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_LinkDialogSave, accessibilityLabel: self.presentationData.strings.Conversation_LinkDialogSave), action: { [weak self] in
if let self, let image = media.media._asMedia() 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: self.context, postbox: self.context.account.postbox, userLocation: self.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
}
}), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuShare, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuShare), action: { [weak self] in
if let self, let image = media.media._asMedia() as? TelegramMediaImage {
self.present(ShareController(context: self.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(self.webPage), media: image), resource: $0.resource)) }))), nil)
}
})], catchTapsOutside: true)
self.present(controller, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in
if let self {
for (_, itemNode) in self.visibleItemsWithNodes {
if let (node, _, _) = itemNode.transitionNode(media: media) {
return (self.scrollNode, node.convert(node.bounds, to: self.scrollNode), self, self.bounds)
}
}
}
return nil
}))
}
@objc private func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, _) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
break
// if let url = self.urlForTapLocation(location) {
// self.openUrl(url)
// }
case .longTap:
break
// 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.presentationData.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, showTranslateIfTopical: false, ignoredLanguages: translationSettings.ignoredLanguages)
//// if canTranslate {
//// actions.append(ContextMenuAction(content: .text(title: strings.Conversation_ContextMenuTranslate, accessibilityLabel: strings.Conversation_ContextMenuTranslate), action: { [weak self] in
//// let controller = TranslateScreen(context: context, text: text, canCopy: true, fromLanguage: language)
//// controller.pushController = { [weak self] c in
//// (self?.controller?.navigationController as? NavigationController)?._keepModalDismissProgress = true
//// self?.controller?.push(c)
//// }
//// controller.presentController = { [weak self] c in
//// self?.controller?.present(c, in: .window(.root))
//// }
//// self?.present(controller, nil)
//// }))
//// }
////
//// let controller = makeContextMenuController(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, sourceLocation: self.sourceLocation, 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?.insets.top ?? 20.0
}
self.scrollNode.view.setContentOffset(CGPoint(x: 0.0, y: targetY), animated: true)
}
} else if case let .Loaded(content) = self.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 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 self {
self.updatePageLayout()
self.updateVisibleItems(visibleBounds: self.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)
}
}
final class BrowserInstantPageContent: UIView, BrowserContent {
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
private var _state: BrowserContentState
private let statePromise: Promise<BrowserContentState>
private let webPage: TelegramMediaWebpage
private var initialized = false
private let instantPageNode: InstantPageContainerNode
var state: Signal<BrowserContentState, NoError> {
return self.statePromise.get()
}
init(context: AccountContext, webPage: TelegramMediaWebpage, url: String, sourceLocation: InstantPageSourceLocation) {
self.webPage = webPage
let title: String
if case let .Loaded(content) = webPage.content {
title = content.title ?? ""
} else {
title = ""
}
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, contentType: .instantPage)
self.statePromise = Promise<BrowserContentState>(self._state)
self.instantPageNode = InstantPageContainerNode(context: context, webPage: webPage, sourceLocation: sourceLocation)
super.init(frame: .zero)
self.addSubnode(self.instantPageNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func reload() {
}
func stop() {
}
func navigateBack() {
}
func navigateForward() {
}
func setFontSize(_ fontSize: CGFloat) {
}
func setForceSerif(_ force: Bool) {
}
func setSearch(_ query: String?, completion: ((Int) -> Void)?) {
}
func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) {
}
func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) {
}
func scrollToTop() {
let scrollView = self.instantPageNode.scrollNode.view
scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: true)
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) {
// let layout = ContainerViewLayout(size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: .portrait), deviceMetrics: .iPhoneX, intrinsicInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: insets.bottom, right: 0.0), safeInsets: UIEdgeInsets(top: 0.0, left: insets.left, bottom: 0.0, right: insets.right), additionalInsets: .zero, statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)
self.instantPageNode.updateLayout(size: size, insets: insets, transition: transition.containedViewLayoutTransition)
self.instantPageNode.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)
//transition.updateFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: 56.0), size: CGSize(width: size.width, height: size.height - 56.0)))
}
}