Local instant view

This commit is contained in:
Ilya Laktyushin 2024-08-29 11:44:05 +04:00
parent 0d1366e5cc
commit 5978a5278d
41 changed files with 3967 additions and 164 deletions

View File

@ -41,6 +41,8 @@ final class BrowserContentState: Equatable {
let contentType: ContentType
let favicon: UIImage?
let isSecure: Bool
let hasInstantView: Bool
let isInnerInstantViewEnabled: Bool
let canGoBack: Bool
let canGoForward: Bool
@ -56,6 +58,8 @@ final class BrowserContentState: Equatable {
contentType: ContentType,
favicon: UIImage? = nil,
isSecure: Bool = false,
hasInstantView: Bool = false,
isInnerInstantViewEnabled: Bool = false,
canGoBack: Bool = false,
canGoForward: Bool = false,
backList: [HistoryItem] = [],
@ -68,6 +72,8 @@ final class BrowserContentState: Equatable {
self.contentType = contentType
self.favicon = favicon
self.isSecure = isSecure
self.hasInstantView = hasInstantView
self.isInnerInstantViewEnabled = isInnerInstantViewEnabled
self.canGoBack = canGoBack
self.canGoForward = canGoForward
self.backList = backList
@ -96,6 +102,9 @@ final class BrowserContentState: Equatable {
if lhs.isSecure != rhs.isSecure {
return false
}
if lhs.hasInstantView != rhs.hasInstantView {
return false
}
if lhs.canGoBack != rhs.canGoBack {
return false
}
@ -112,43 +121,51 @@ final class BrowserContentState: Equatable {
}
func withUpdatedTitle(_ title: String) -> BrowserContentState {
return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
return BrowserContentState(title: title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, hasInstantView: self.hasInstantView, isInnerInstantViewEnabled: self.isInnerInstantViewEnabled, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedUrl(_ url: String) -> BrowserContentState {
return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
return BrowserContentState(title: self.title, url: url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, hasInstantView: self.hasInstantView, isInnerInstantViewEnabled: self.isInnerInstantViewEnabled, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedIsSecure(_ isSecure: Bool) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: isSecure, hasInstantView: self.hasInstantView, isInnerInstantViewEnabled: self.isInnerInstantViewEnabled, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedHasInstantView(_ hasInstantView: Bool) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, hasInstantView: hasInstantView, isInnerInstantViewEnabled: self.isInnerInstantViewEnabled, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedIsInnerInstantViewEnabled(_ isInnerInstantViewEnabled: Bool) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, hasInstantView: self.hasInstantView, isInnerInstantViewEnabled: isInnerInstantViewEnabled, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedEstimatedProgress(_ estimatedProgress: Double) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, hasInstantView: self.hasInstantView, isInnerInstantViewEnabled: self.isInnerInstantViewEnabled, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedReadingProgress(_ readingProgress: Double) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, hasInstantView: self.hasInstantView, isInnerInstantViewEnabled: self.isInnerInstantViewEnabled, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedFavicon(_ favicon: UIImage?) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: favicon, isSecure: self.isSecure, hasInstantView: self.hasInstantView, isInnerInstantViewEnabled: self.isInnerInstantViewEnabled, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedCanGoBack(_ canGoBack: Bool) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, hasInstantView: self.hasInstantView, isInnerInstantViewEnabled: self.isInnerInstantViewEnabled, canGoBack: canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedCanGoForward(_ canGoForward: Bool) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: canGoForward, backList: self.backList, forwardList: self.forwardList)
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, hasInstantView: self.hasInstantView, isInnerInstantViewEnabled: self.isInnerInstantViewEnabled, canGoBack: self.canGoBack, canGoForward: canGoForward, backList: self.backList, forwardList: self.forwardList)
}
func withUpdatedBackList(_ backList: [HistoryItem]) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: backList, forwardList: self.forwardList)
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, hasInstantView: self.hasInstantView, isInnerInstantViewEnabled: self.isInnerInstantViewEnabled, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: backList, forwardList: self.forwardList)
}
func withUpdatedForwardList(_ forwardList: [HistoryItem]) -> BrowserContentState {
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: forwardList)
return BrowserContentState(title: self.title, url: self.url, estimatedProgress: self.estimatedProgress, readingProgress: self.readingProgress, contentType: self.contentType, favicon: self.favicon, isSecure: self.isSecure, hasInstantView: self.hasInstantView, isInnerInstantViewEnabled: self.isInnerInstantViewEnabled, canGoBack: self.canGoBack, canGoForward: self.canGoForward, backList: self.backList, forwardList: forwardList)
}
}
@ -158,7 +175,7 @@ protocol BrowserContent: UIView {
var currentState: BrowserContentState { get }
var state: Signal<BrowserContentState, NoError> { get }
var pushContent: (BrowserScreen.Subject) -> Void { get set }
var pushContent: (BrowserScreen.Subject, BrowserContent?) -> Void { get set }
var present: (ViewController, Any?) -> Void { get set }
var presentInGlobalOverlay: (ViewController) -> Void { get set }
var getNavigationController: () -> NavigationController? { get set }
@ -177,6 +194,8 @@ protocol BrowserContent: UIView {
func navigateForward()
func navigateTo(historyItem: BrowserContentState.HistoryItem)
func toggleInstantView(_ enabled: Bool)
func updatePresentationData(_ presentationData: PresentationData)
func updateFontState(_ state: BrowserPresentationState.FontState)

View File

@ -36,7 +36,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate
return self.statePromise.get()
}
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
var pushContent: (BrowserScreen.Subject, BrowserContent?) -> Void = { _, _ in }
var openAppUrl: (String) -> Void = { _ in }
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
var minimize: () -> Void = { }
@ -122,6 +122,9 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate
self.webView.evaluateJavaScript(js) { _, _ in }
}
func toggleInstantView(_ enabled: Bool) {
}
private var didSetupSearch = false
private func setupSearch(completion: @escaping () -> Void) {
guard !self.didSetupSearch else {
@ -385,7 +388,7 @@ final class BrowserDocumentContent: UIView, BrowserContent, WKNavigationDelegate
navigationController._keepModalDismissProgress = true
navigationController.pushViewController(controller)
} else {
self.pushContent(subject)
self.pushContent(subject, nil)
}
}

View File

@ -28,6 +28,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
private var theme: InstantPageTheme
private var settings: InstantPagePresentationSettings = .defaultSettings
private let sourceLocation: InstantPageSourceLocation
private let preloadedResouces: [Any]?
private var originalContent: BrowserContent?
private var webPage: TelegramMediaWebpage?
@ -66,7 +68,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
var currentAccessibilityAreas: [AccessibilityAreaNode] = []
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
var pushContent: (BrowserScreen.Subject, BrowserContent?) -> Void = { _, _ in }
var restoreContent: (BrowserContent) -> Void = { _ in }
var openAppUrl: (String) -> Void = { _ in }
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
var minimize: () -> Void = { }
@ -84,19 +87,21 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
private let loadWebpageDisposable = MetaDisposable()
private let resolveUrlDisposable = MetaDisposable()
private let updateLayoutDisposable = MetaDisposable()
private let loadProgress = ValuePromise<CGFloat>(1.0, ignoreRepeated: true)
private let readingProgress = ValuePromise<CGFloat>(1.0, ignoreRepeated: true)
private var containerLayout: (size: CGSize, insets: UIEdgeInsets, fullInsets: UIEdgeInsets)?
private var setupScrollOffsetOnLayout = false
init(context: AccountContext, presentationData: PresentationData, webPage: TelegramMediaWebpage, anchor: String?, url: String, sourceLocation: InstantPageSourceLocation) {
init(context: AccountContext, presentationData: PresentationData, webPage: TelegramMediaWebpage, anchor: String?, url: String, sourceLocation: InstantPageSourceLocation, preloadedResouces: [Any]?, originalContent: BrowserContent? = nil) {
self.context = context
self.webPage = webPage
self.presentationData = presentationData
self.theme = instantPageThemeForType(presentationData.theme.overallDarkAppearance ? .dark : .light, settings: .defaultSettings)
self.sourceLocation = sourceLocation
self.preloadedResouces = preloadedResouces
self.originalContent = originalContent
self.uuid = UUID()
@ -107,7 +112,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
title = ""
}
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .instantPage)
let isInnerInstantViewEnabled = originalContent != nil
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .instantPage, isInnerInstantViewEnabled: isInnerInstantViewEnabled)
self.statePromise = Promise<BrowserContentState>(self._state)
self.wrapperNode = ASDisplayNode()
@ -126,7 +132,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
self.readingProgress.get()
)
|> map { estimatedProgress, readingProgress in
return BrowserContentState(title: title, url: url, estimatedProgress: estimatedProgress, readingProgress: readingProgress, contentType: .instantPage)
return BrowserContentState(title: title, url: url, estimatedProgress: estimatedProgress, readingProgress: readingProgress, contentType: .instantPage, isInnerInstantViewEnabled: isInnerInstantViewEnabled)
}
))
@ -359,6 +365,12 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
self.updatePageLayout()
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
}
func toggleInstantView(_ enabled: Bool) {
if !enabled, let originalContent = self.originalContent {
self.restoreContent(originalContent)
}
}
func setSearch(_ query: String?, completion: ((Int) -> Void)?) {
@ -603,7 +615,22 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
self?.updateWebEmbedHeight(embedIndex, height)
}, updateDetailsExpanded: { [weak self] expanded in
self?.updateDetailsExpanded(detailsIndex, expanded)
}, currentExpandedDetails: self.currentExpandedDetails) {
}, currentExpandedDetails: self.currentExpandedDetails, getPreloadedResource: { [weak self] url in
if let preloadedResouces = self?.preloadedResouces {
var cleanUrl = url
var components = URLComponents(string: url)
components?.queryItems = nil
cleanUrl = components?.url?.absoluteString ?? cleanUrl
for resource in preloadedResouces {
if let resource = resource as? [String: Any], let resourceUrl = resource["WebResourceURL"] as? String {
if resourceUrl == url || resourceUrl.hasPrefix(cleanUrl) {
return resource["WebResourceData"] as? Data
}
}
}
}
return nil
}) {
newNode.frame = itemFrame
newNode.updateLayout(size: itemFrame.size, transition: transition)
if let topNode = topNode {
@ -878,7 +905,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
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 {
if let webPage = self.webPage, case let .Loaded(content) = webPage.content, let page = content.instantPage, page.url == baseUrl || baseUrl.isEmpty, let anchor = anchor {
self.scrollToAnchor(anchor)
return
}
@ -905,7 +932,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
case let .result(webpageResult):
if let webpageResult = webpageResult, case .Loaded = webpageResult.webpage.content {
strongSelf.loadProgress.set(1.0)
strongSelf.pushContent(.instantPage(webPage: webpageResult.webpage, anchor: anchor, sourceLocation: strongSelf.sourceLocation))
strongSelf.pushContent(.instantPage(webPage: webpageResult.webpage, anchor: anchor, sourceLocation: strongSelf.sourceLocation, preloadedResources: nil), nil)
}
break
case let .progress(progress):
@ -915,11 +942,11 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
}))
} else {
strongSelf.loadProgress.set(1.0)
strongSelf.pushContent(.webPage(url: externalUrl))
strongSelf.pushContent(.webPage(url: externalUrl), nil)
}
case let .instantView(webpage, anchor):
strongSelf.loadProgress.set(1.0)
strongSelf.pushContent(.instantPage(webPage: webpage, anchor: anchor, sourceLocation: strongSelf.sourceLocation))
strongSelf.pushContent(.instantPage(webPage: webpage, anchor: anchor, sourceLocation: strongSelf.sourceLocation, preloadedResources: nil), nil)
default:
strongSelf.loadProgress.set(1.0)
strongSelf.minimize()

View File

@ -24,7 +24,12 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
private let pdfView: PDFView
private let scrollView: UIScrollView!
private let pageIndicatorBackgorund: UIVisualEffectView
private let pageIndicator = ComponentView<Empty>()
private var pageNumber: (Int, Int)?
private var pageTimer: SwiftSignalKit.Timer?
let uuid: UUID
private var _state: BrowserContentState
@ -37,7 +42,7 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
return self.statePromise.get()
}
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
var pushContent: (BrowserScreen.Subject, BrowserContent?) -> Void = { _, _ in }
var openAppUrl: (String) -> Void = { _ in }
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
var minimize: () -> Void = { }
@ -57,6 +62,10 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
self.pdfView = PDFView()
self.pdfView.clipsToBounds = false
self.pageIndicatorBackgorund = UIVisualEffectView(effect: UIBlurEffect(style: .light))
self.pageIndicatorBackgorund.clipsToBounds = true
self.pageIndicatorBackgorund.layer.cornerRadius = 10.0
var scrollView: UIScrollView?
for view in self.pdfView.subviews {
if let view = view as? UIScrollView {
@ -104,6 +113,10 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
scrollView.delegate = self
}
}
self.pageNumber = (1, self.pdfView.document?.pageCount ?? 1)
self.startPageIndicatorTimer()
}
required init?(coder: NSCoder) {
@ -120,12 +133,27 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
}
}
func startPageIndicatorTimer() {
self.pageTimer?.invalidate()
self.pageTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: false, completion: { [weak self] in
guard let self else {
return
}
let transition = ComponentTransition.easeInOut(duration: 0.25)
transition.setAlpha(view: self.pageIndicatorBackgorund, alpha: 0.0)
}, queue: Queue.mainQueue())
self.pageTimer?.start()
}
func updateFontState(_ state: BrowserPresentationState.FontState) {
}
func updateFontState(_ state: BrowserPresentationState.FontState, force: Bool) {
}
func toggleInstantView(_ enabled: Bool) {
}
private var findSession: Any?
private var previousQuery: String?
@ -308,6 +336,29 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
let pdfViewFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: size.width - insets.left - insets.right, height: size.height - insets.top - bottomInset))
transition.setFrame(view: self.pdfView, frame: pdfViewFrame)
let pageIndicatorSize = self.pageIndicator.update(
transition: .immediate,
component: AnyComponent(
Text(text: "\(self.pageNumber?.0 ?? 1) of \(self.pageNumber?.1 ?? 1)", font: Font.with(size: 15.0, weight: .semibold, traits: .monospacedNumbers), color: self.presentationData.theme.list.itemSecondaryTextColor)
),
environment: {},
containerSize: size
)
if let view = self.pageIndicator.view {
if view.superview == nil {
self.addSubview(self.pageIndicatorBackgorund)
self.pageIndicatorBackgorund.contentView.addSubview(view)
}
let horizontalPadding: CGFloat = 10.0
let verticalPadding: CGFloat = 8.0
let pageBackgroundFrame = CGRect(origin: CGPoint(x: insets.left + 20.0, y: insets.top + 16.0), size: CGSize(width: horizontalPadding * 2.0 + pageIndicatorSize.width, height: verticalPadding * 2.0 + pageIndicatorSize.height))
self.pageIndicatorBackgorund.bounds = CGRect(origin: .zero, size: pageBackgroundFrame.size)
transition.setPosition(view: self.pageIndicatorBackgorund, position: pageBackgroundFrame.center)
view.frame = CGRect(origin: CGPoint(x: horizontalPadding, y: verticalPadding), size: pageIndicatorSize)
}
if isFirstTime {
self.pdfView.setNeedsLayout()
self.pdfView.layoutIfNeeded()
@ -370,9 +421,31 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
}
if !scrollView.isZooming && !self.wasZooming {
self.updateScrollingOffset(isReset: false, transition: .immediate)
if let document = self.pdfView.document, let page = self.pdfView.currentPage {
let number = document.index(for: page) + 1
if number != self.pageNumber?.0 {
self.pageNumber = (number, document.pageCount)
if let (size, insets, fullInsets) = self.validLayout {
self.updateLayout(size: size, insets: insets, fullInsets: fullInsets, safeInsets: .zero, transition: .immediate)
}
}
}
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if let scrollViewDelegate = scrollView as? UIScrollViewDelegate {
scrollViewDelegate.scrollViewWillBeginDragging?(scrollView)
}
let transition = ComponentTransition.easeInOut(duration: 0.1)
transition.setAlpha(view: self.pageIndicatorBackgorund, alpha: 1.0)
self.pageTimer?.invalidate()
self.pageTimer = nil
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if let scrollViewDelegate = scrollView as? UIScrollViewDelegate {
scrollViewDelegate.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
@ -383,6 +456,8 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
if self.ignoreUpdatesUntilScrollingStopped {
self.ignoreUpdatesUntilScrollingStopped = false
}
self.startPageIndicatorTimer()
}
}
@ -395,6 +470,8 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
if self.ignoreUpdatesUntilScrollingStopped {
self.ignoreUpdatesUntilScrollingStopped = false
}
self.startPageIndicatorTimer()
}
private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) {
@ -449,7 +526,7 @@ final class BrowserPdfContent: UIView, BrowserContent, UIScrollViewDelegate, PDF
navigationController._keepModalDismissProgress = true
navigationController.pushViewController(controller)
} else {
self.pushContent(subject)
self.pushContent(subject, nil)
}
}

View File

@ -0,0 +1,585 @@
import Foundation
import WebKit
import AppBundle
import Postbox
import TelegramCore
import InstantPageUI
public class Readability: NSObject, WKNavigationDelegate {
private let url: URL
let webView: WKWebView
private let completionHandler: ((_ webPage: (TelegramMediaWebpage, [Any]?)?, _ error: Error?) -> Void)
private var hasRenderedReadabilityHTML = false
private var subresources: [Any]?
init(url: URL, archiveData: Data, completionHandler: @escaping (_ webPage: (TelegramMediaWebpage, [Any]?)?, _ error: Error?) -> Void) {
self.url = url
self.completionHandler = completionHandler
let preferences = WKPreferences()
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
configuration.userContentController.addUserScript(ReadabilityUserScript())
self.webView = WKWebView(frame: CGRect.zero, configuration: configuration)
super.init()
self.webView.configuration.suppressesIncrementalRendering = true
self.webView.navigationDelegate = self
if #available(iOS 16.4, *) {
self.webView.isInspectable = true
}
if let (html, subresources) = extractHtmlString(from: archiveData) {
self.subresources = subresources
self.webView.loadHTMLString(html, baseURL: url.baseURL)
}
}
private func initializeReadability(completion: @escaping (_ result: TelegramMediaWebpage?, _ error: Error?) -> Void) {
guard let readabilityInitializationJS = loadFile(name: "ReaderMode", type: "js") else {
return
}
self.webView.evaluateJavaScript(readabilityInitializationJS) { (result, error) in
guard let result = result as? [String: Any] else {
completion(nil, error)
return
}
guard let page = parseJson(result, url: self.url.absoluteString) else {
return
}
completion(page, nil)
}
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if !self.hasRenderedReadabilityHTML {
self.initializeReadability() { [weak self] (webPage: TelegramMediaWebpage?, error: Error?) in
guard let self else {
return
}
self.hasRenderedReadabilityHTML = true
guard let webPage else {
self.completionHandler(nil, error)
return
}
self.completionHandler((webPage, self.subresources), error)
}
}
}
}
class ReadabilityUserScript: WKUserScript {
convenience override init() {
guard let js = loadFile(name: "Readability", type: "js") else {
fatalError()
}
self.init(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
}
}
func loadFile(name: String, type: String) -> String? {
let bundle = getAppBundle()
guard let userScriptPath = bundle.path(forResource: name, ofType: type) else {
return nil
}
guard let userScriptData = try? Data(contentsOf: URL(fileURLWithPath: userScriptPath)) else {
return nil
}
guard let userScript = String(data: userScriptData, encoding: .utf8) else {
return nil
}
return userScript
}
private func extractHtmlString(from webArchiveData: Data) -> (String, [Any]?)? {
if let webArchiveDict = try? PropertyListSerialization.propertyList(from: webArchiveData, format: nil) as? [String: Any],
let mainResource = webArchiveDict["WebMainResource"] as? [String: Any],
let htmlData = mainResource["WebResourceData"] as? Data {
guard let htmlString = String(data: htmlData, encoding: .utf8) else {
return nil
}
return (htmlString, webArchiveDict["WebSubresources"] as? [Any])
}
return nil
}
private func parseJson(_ input: [String: Any], url: String) -> TelegramMediaWebpage? {
let siteName = input["siteName"] as? String
let title = input["title"] as? String
let byline = input["byline"] as? String
let excerpt = input["excerpt"] as? String
var media: [MediaId: Media] = [:]
let blocks = parseContent(input, url, &media)
guard !blocks.isEmpty else {
return nil
}
return TelegramMediaWebpage(
webpageId: MediaId(namespace: 0, id: 0),
content: .Loaded(
TelegramMediaWebpageLoadedContent(
url: url,
displayUrl: url,
hash: 0,
type: "article",
websiteName: siteName,
title: title,
text: excerpt,
embedUrl: nil,
embedType: nil,
embedSize: nil,
duration: nil,
author: byline,
isMediaLargeByDefault: nil,
image: nil,
file: nil,
story: nil,
attributes: [],
instantPage: InstantPage(
blocks: blocks,
media: media,
isComplete: true,
rtl: false,
url: url,
views: nil
)
)
)
)
}
private func parseContent(_ input: [String: Any], _ url: String, _ media: inout [MediaId: Media]) -> [InstantPageBlock] {
let title = input["title"] as? String
let byline = input["byline"] as? String
let date = input["publishedTime"] as? String
let _ = date
guard let content = input["content"] as? [Any] else {
return []
}
var blocks = parsePageBlocks(content, url, &media)
if case .header = blocks.first {
} else {
if var byline {
byline = byline.replacingOccurrences(of: "[\n\t]+", with: " ", options: .regularExpression, range: nil)
blocks.insert(.authorDate(author: trim(parseRichText(byline)), date: 0), at: 0)
}
if let title {
blocks.insert(.title(trim(parseRichText(title))), at: 0)
}
}
return blocks
}
private func parseRichText(_ input: String) -> RichText {
return .plain(input)
}
private func parseRichText(_ input: [String: Any], _ media: inout [MediaId: Media]) -> RichText {
var text: RichText
if let string = input["content"] as? String {
text = parseRichText(string)
} else if let array = input["content"] as? [Any] {
text = parseRichText(array, &media)
} else {
text = .empty
}
text = applyAnchor(text, item: input)
if let _ = input["bold"] {
text = .bold(text)
}
if let _ = input["italic"] {
text = .italic(text)
}
return text
}
private func parseRichText(_ input: [Any], _ media: inout [MediaId: Media]) -> RichText {
var result: [RichText] = []
for item in input {
if let string = item as? String {
result.append(parseRichText(string))
} else if let item = item as? [String: Any], let tag = item["tag"] as? String {
var text: RichText?
switch tag {
case "b", "strong":
text = .bold(parseRichText(item, &media))
case "i":
text = .italic(parseRichText(item, &media))
case "s":
text = .strikethrough(parseRichText(item, &media))
case "p":
text = parseRichText(item, &media)
case "a":
if let href = item["href"] as? String {
let telString = "tel:"
let mailtoString = "mailto:"
if href.hasPrefix("tel:") {
text = .phone(text: parseRichText(item, &media), phone: String(href[href.index(href.startIndex, offsetBy: telString.distance(from: telString.startIndex, to: telString.endIndex))...]))
} else if href.hasPrefix(mailtoString) {
text = .email(text: parseRichText(item, &media), email: String(href[href.index(href.startIndex, offsetBy: mailtoString.distance(from: mailtoString.startIndex, to: mailtoString.endIndex))...]))
} else {
text = .url(text: parseRichText(item, &media), url: href, webpageId: nil)
}
} else {
text = parseRichText(item, &media)
}
case "pre", "code":
text = .fixed(parseRichText(item, &media))
case "mark":
text = .marked(parseRichText(item, &media))
case "sub":
text = .subscript(parseRichText(item, &media))
case "sup":
text = .superscript(parseRichText(item, &media))
case "img":
if let src = item["src"] as? String, !src.isEmpty {
let width: Int32
if let value = item["width"] as? String, let intValue = Int32(value) {
width = intValue
} else {
width = 0
}
let height: Int32
if let value = item["height"] as? String, let intValue = Int32(value) {
height = intValue
} else {
height = 0
}
let id = MediaId(namespace: Namespaces.Media.CloudFile, id: Int64(media.count))
media[id] = TelegramMediaImage(
imageId: id,
representations: [
TelegramMediaImageRepresentation(
dimensions: PixelDimensions(width: width, height: height),
resource: InstantPageExternalMediaResource(url: src),
progressiveSizes: [],
immediateThumbnailData: nil
)
],
immediateThumbnailData: nil,
reference: nil,
partialReference: nil,
flags: []
)
text = .image(id: id, dimensions: PixelDimensions(width: width, height: height))
}
case "br":
if let last = result.last {
result[result.count - 1] = addNewLine(last)
}
default:
text = parseRichText(item, &media)
}
if var text {
text = applyAnchor(text, item: item)
result.append(text)
}
}
}
if !result.isEmpty {
return .concat(result)
} else if result.count == 1, let text = result.first {
return text
} else {
return .empty
}
}
private func trimStart(_ input: RichText) -> RichText {
return input
}
private func trimEnd(_ input: RichText) -> RichText {
return input
}
private func trim(_ input: RichText) -> RichText {
return input
}
private func addNewLine(_ input: RichText) -> RichText {
var text = input
switch input {
case .empty:
text = .empty
case let .plain(string):
text = .plain(string + "\n")
case let .bold(richText):
text = .bold(addNewLine(richText))
case let .italic(richText):
text = .italic(addNewLine(richText))
case let .underline(richText):
text = .underline(addNewLine(richText))
case let .strikethrough(richText):
text = .strikethrough(addNewLine(richText))
case let .fixed(richText):
text = .fixed(addNewLine(richText))
case let .url(richText, url, webpageId):
text = .url(text: addNewLine(richText), url: url, webpageId: webpageId)
case let .email(richText, email):
text = .email(text: addNewLine(richText), email: email)
case let .subscript(richText):
text = .subscript(addNewLine(richText))
case let .superscript(richText):
text = .superscript(addNewLine(richText))
case let .marked(richText):
text = .marked(addNewLine(richText))
case let .phone(richText, phone):
text = .phone(text: addNewLine(richText), phone: phone)
case let .anchor(richText, name):
text = .anchor(text: addNewLine(richText), name: name)
case var .concat(array):
array[array.count - 1] = addNewLine(array[array.count - 1])
text = .concat(array)
case .image:
break
}
return text
}
private func applyAnchor(_ input: RichText, item: [String: Any]) -> RichText {
guard let id = item["id"] as? String, !id.isEmpty else {
return input
}
return .anchor(text: input, name: id)
}
private func parseTable(_ input: [String: Any], _ media: inout [MediaId: Media]) -> InstantPageBlock {
let title = (input["title"] as? String) ?? ""
return .table(
title: trim(applyAnchor(parseRichText(title), item: input)),
rows: parseTableRows((input["content"] as? [Any]) ?? [], &media),
bordered: true,
striped: true
)
}
private func parseTableRows(_ input: [Any], _ media: inout [MediaId: Media]) -> [InstantPageTableRow] {
var result: [InstantPageTableRow] = []
for item in input {
if let item = item as? [String: Any] {
let tag = item["tag"] as? String
if tag == "tr" {
result.append(parseTableRow(item, &media))
} else if let content = item["content"] as? [Any] {
result.append(contentsOf: parseTableRows(content, &media))
}
}
}
return result
}
private func parseTableRow(_ input: [String: Any], _ media: inout [MediaId: Media]) -> InstantPageTableRow {
var cells: [InstantPageTableCell] = []
if let content = input["content"] as? [Any] {
for item in content {
guard let item = item as? [String: Any] else {
continue
}
let tag = item["tag"] as? String
guard ["td", "th"].contains(tag) else {
continue
}
var text: RichText?
if let content = item["content"] as? [Any] {
text = trim(parseRichText(content, &media))
if let currentText = text {
if let _ = item["bold"] {
text = .bold(currentText)
}
if let _ = item["italic"] {
text = .italic(currentText)
}
}
}
cells.append(InstantPageTableCell(
text: text,
header: tag == "th",
alignment: item["xcenter"] != nil ? .center : .left,
verticalAlignment: .middle,
colspan: ((item["colspan"] as? String).flatMap { Int32($0) }) ?? 0,
rowspan: ((item["rowspan"] as? String).flatMap { Int32($0) }) ?? 0
))
}
}
return InstantPageTableRow(cells: cells)
}
private func parseDetails(_ item: [String: Any], _ url: String, _ media: inout [MediaId: Media]) -> InstantPageBlock? {
guard var content = item["contant"] as? [Any] else {
return nil
}
var title: RichText = .empty
var titleIndex: Int?
for i in 0 ..< content.count {
if let subitem = content[i] as? [String: Any], let tag = subitem["tag"] as? String, tag == "summary" {
title = trim(parseRichText(subitem, &media))
titleIndex = i
break
}
}
if let titleIndex {
content.remove(at: titleIndex)
}
return .details(
title: title,
blocks: parsePageBlocks(content, url, &media),
expanded: item["open"] != nil
)
}
private func parseList(_ input: [String: Any], _ media: inout [MediaId: Media]) -> InstantPageBlock? {
guard let content = input["content"] as? [Any], let tag = input["tag"] as? String else {
return nil
}
var items: [InstantPageListItem] = []
for item in content {
guard let item = item as? [String: Any], let tag = item["tag"] as? String, tag == "li" else {
continue
}
items.append(.text(trim(parseRichText(item, &media)), nil))
}
let ordered = tag == "ol"
return .list(items: items, ordered: ordered)
}
private func parseImage(_ input: [String: Any], _ media: inout [MediaId: Media]) -> InstantPageBlock? {
guard let src = input["src"] as? String else {
return nil
}
let caption: InstantPageCaption
if let alt = input["alt"] as? String {
caption = InstantPageCaption(
text: trim(parseRichText(alt)),
credit: .empty
)
} else {
caption = InstantPageCaption(text: .empty, credit: .empty)
}
let width: Int32
if let value = input["width"] as? String, let intValue = Int32(value) {
width = intValue
} else {
width = 0
}
let height: Int32
if let value = input["height"] as? String, let intValue = Int32(value) {
height = intValue
} else {
height = 0
}
let id = MediaId(namespace: Namespaces.Media.CloudImage, id: Int64(media.count))
media[id] = TelegramMediaImage(
imageId: id,
representations: [
TelegramMediaImageRepresentation(
dimensions: PixelDimensions(width: width, height: height),
resource: InstantPageExternalMediaResource(url: src),
progressiveSizes: [],
immediateThumbnailData: nil
)
],
immediateThumbnailData: nil,
reference: nil,
partialReference: nil,
flags: []
)
return .image(
id: id,
caption: caption,
url: nil,
webpageId: nil
)
}
private func parseFigure(_ input: [String: Any], _ media: inout [MediaId: Media]) -> InstantPageBlock? {
guard let content = input["content"] as? [Any] else {
return nil
}
var block: InstantPageBlock?
var caption: RichText?
for item in content {
if let item = item as? [String: Any], let tag = item["tag"] as? String {
if tag == "img" {
block = parseImage(item, &media)
} else if tag == "figurecaption" {
caption = trim(parseRichText(item, &media))
}
}
}
guard var block else {
return nil
}
if let caption, case let .image(id, _, url, webpageId) = block {
block = .image(id: id, caption: InstantPageCaption(text: caption, credit: .empty), url: url, webpageId: webpageId)
}
return block
}
private func parsePageBlocks(_ input: [Any], _ url: String, _ media: inout [MediaId: Media]) -> [InstantPageBlock] {
var result: [InstantPageBlock] = []
for item in input {
if let string = item as? String {
result.append(.paragraph(parseRichText(string)))
} else if let item = item as? [String: Any], let tag = item["tag"] as? String {
let content = item["content"] as? [Any]
switch tag {
case "p":
result.append(.paragraph(trim(parseRichText(item, &media))))
case "h1", "h2":
result.append(.header(trim(parseRichText(item, &media))))
case "h3", "h4", "h5", "h6":
result.append(.subheader(trim(parseRichText(item, &media))))
case "pre":
result.append(.preformatted(.fixed(trim(parseRichText(item, &media)))))
case "blockquote":
result.append(.blockQuote(text: .italic(trim(parseRichText(item, &media))), caption: .empty))
case "img":
if let image = parseImage(item, &media) {
result.append(image)
}
break
case "figure":
if let figure = parseFigure(item, &media) {
result.append(figure)
}
case "table":
result.append(parseTable(item, &media))
case "ul", "ol":
if let list = parseList(item, &media) {
result.append(list)
}
case "hr":
result.append(.divider)
case "details":
if let details = parseDetails(item, url, &media) {
result.append(details)
}
default:
if let content {
result.append(contentsOf: parsePageBlocks(content, url, &media))
}
}
}
}
return result
}

View File

@ -492,6 +492,7 @@ public class BrowserScreen: ViewController, MinimizableController {
case increaseFontSize
case resetFontSize
case updateFontIsSerif(Bool)
case toggleInstantView(Bool)
case addBookmark
case openBookmarks
case openAddressBar
@ -519,7 +520,7 @@ public class BrowserScreen: ViewController, MinimizableController {
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private var validLayout: (ContainerViewLayout, CGFloat)?
init(controller: BrowserScreen) {
self.context = controller.context
self.controller = controller
@ -755,6 +756,8 @@ public class BrowserScreen: ViewController, MinimizableController {
return updatedState
})
content.updateFontState(self.presentationState.fontState)
case let .toggleInstantView(enabled):
content.toggleInstantView(enabled)
case .addBookmark:
if let content = self.content.last {
self.addBookmark(content.currentState.url, showArrow: true)
@ -822,7 +825,7 @@ public class BrowserScreen: ViewController, MinimizableController {
self.requestLayout(transition: transition)
}
func pushContent(_ content: BrowserScreen.Subject, transition: ComponentTransition) {
func pushContent(_ content: BrowserScreen.Subject, additionalContent: BrowserContent? = nil, transition: ComponentTransition) {
let browserContent: BrowserContent
switch content {
case let .webPage(url):
@ -834,25 +837,37 @@ public class BrowserScreen: ViewController, MinimizableController {
}
browserContent = webContent
self.controller?.preferredConfiguration = nil
case let .instantPage(webPage, anchor, sourceLocation):
let instantPageContent = BrowserInstantPageContent(context: self.context, presentationData: self.presentationData, webPage: webPage, anchor: anchor, url: webPage.content.url ?? "", sourceLocation: sourceLocation)
case let .instantPage(webPage, anchor, sourceLocation, preloadedResouces):
let instantPageContent = BrowserInstantPageContent(context: self.context, presentationData: self.presentationData, webPage: webPage, anchor: anchor, url: webPage.content.url ?? "", sourceLocation: sourceLocation, preloadedResouces: preloadedResouces, originalContent: additionalContent)
instantPageContent.openPeer = { [weak self] peer in
guard let self else {
return
}
self.openPeer(peer)
}
instantPageContent.restoreContent = { [weak self, weak instantPageContent] content in
guard let self, let instantPageContent else {
return
}
self.pushBrowserContent(content, additionalContent: instantPageContent, transition: .easeInOut(duration: 0.3).withUserData(NavigationStackComponent<Empty>.CurlTransition.hide))
}
browserContent = instantPageContent
case let .document(file, _):
browserContent = BrowserDocumentContent(context: self.context, presentationData: self.presentationData, file: file)
case let .pdfDocument(file, _):
browserContent = BrowserPdfContent(context: self.context, presentationData: self.presentationData, file: file)
}
browserContent.pushContent = { [weak self] content in
browserContent.pushContent = { [weak self] content, additionalContent in
guard let self else {
return
}
self.pushContent(content, transition: .spring(duration: 0.4))
var transition: ComponentTransition
if let _ = additionalContent {
transition = .easeInOut(duration: 0.3).withUserData(NavigationStackComponent<Empty>.CurlTransition.show)
} else {
transition = .spring(duration: 0.4)
}
self.pushContent(content, additionalContent: additionalContent, transition: transition)
}
browserContent.openAppUrl = { [weak self] url in
guard let self else {
@ -896,7 +911,15 @@ public class BrowserScreen: ViewController, MinimizableController {
}
}
self.content.append(browserContent)
self.pushBrowserContent(browserContent, additionalContent: additionalContent, transition: transition)
}
func pushBrowserContent(_ browserContent: BrowserContent, additionalContent: BrowserContent? = nil, transition: ComponentTransition) {
if let additionalContent, let index = self.content.firstIndex(where: { $0 === additionalContent }) {
self.content[index] = browserContent
} else {
self.content.append(browserContent)
}
self.requestLayout(transition: transition)
self.setupContentStateUpdates()
@ -1050,16 +1073,20 @@ public class BrowserScreen: ViewController, MinimizableController {
return
}
guard let controller = self.controller, let content = self.content.last else {
return
}
if let animationComponentView = referenceView.componentView.view as? LottieComponent.View {
animationComponentView.playOnce()
}
self.view.endEditing(true)
let checkIcon: (PresentationTheme) -> UIImage? = { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Check"), color: theme.contextMenu.primaryColor) }
let emptyIcon: (PresentationTheme) -> UIImage? = { _ in
return nil
}
// let checkIcon: (PresentationTheme) -> UIImage? = { theme in return generateTintedImage(image: UIImage(bundleImageName: "Instant View/Settings/Check"), color: theme.contextMenu.primaryColor) }
// let emptyIcon: (PresentationTheme) -> UIImage? = { _ in
// return nil
// }
let settings = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.webBrowserSettings])
|> take(1)
@ -1071,17 +1098,21 @@ public class BrowserScreen: ViewController, MinimizableController {
}
}
let _ = (settings
|> deliverOnMainQueue).start(next: { [weak self] settings in
guard let self, let controller = self.controller, let contentState = self.contentState, let layout = self.validLayout?.0 else {
return
let source: ContextContentSource = .reference(BrowserReferenceContentSource(controller: controller, sourceView: referenceView.referenceNode.view))
let items: Signal<ContextController.Items, NoError> = combineLatest(
queue: Queue.mainQueue(),
settings,
content.state
)
|> map { [weak self] settings, contentState -> ContextController.Items in
guard let self, let layout = self.validLayout?.0 else {
return ContextController.Items(content: .list([]))
}
let source: ContextContentSource = .reference(BrowserReferenceContentSource(controller: controller, sourceView: referenceView.referenceNode.view))
let performAction = self.performAction
let forceIsSerif = self.presentationState.fontState.isSerif
// let forceIsSerif = self.presentationState.fontState.isSerif
let fontItem = BrowserFontSizeContextMenuItem(
value: self.presentationState.fontState.size,
decrease: { [weak self] in
@ -1140,16 +1171,20 @@ public class BrowserScreen: ViewController, MinimizableController {
} else {
items.append(.custom(fontItem, false))
//TODO:localize
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontSanFrancisco, icon: forceIsSerif ? emptyIcon : checkIcon, action: { (controller, action) in
performAction.invoke(.updateFontIsSerif(false))
action(.default)
})))
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.InstantPage_FontNewYork, textFont: .custom(font: Font.with(size: 17.0, design: .serif, traits: []), height: nil, verticalOffset: nil), icon: forceIsSerif ? checkIcon : emptyIcon, action: { (controller, action) in
performAction.invoke(.updateFontIsSerif(true))
action(.default)
})))
if case .webPage = contentState.contentType {
let isAvailable = contentState.hasInstantView
items.append(.action(ContextMenuActionItem(text: "Show Instant View", textColor: isAvailable ? .primary : .disabled, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Boost"), color: isAvailable ? theme.contextMenu.primaryColor : theme.contextMenu.primaryColor.withAlphaComponent(0.3)) }, action: isAvailable ? { (controller, action) in
performAction.invoke(.toggleInstantView(true))
action(.default)
} : nil)))
} else if case .instantPage = contentState.contentType, contentState.isInnerInstantViewEnabled {
items.append(.action(ContextMenuActionItem(text: "Hide Instant View", textColor: .primary, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Boost"), color: theme.contextMenu.primaryColor) }, action: { (controller, action) in
performAction.invoke(.toggleInstantView(false))
action(.default)
})))
}
}
if !items.isEmpty {
@ -1190,10 +1225,11 @@ public class BrowserScreen: ViewController, MinimizableController {
})))
}
}
let contextController = ContextController(presentationData: self.presentationData, source: source, items: .single(ContextController.Items(content: .list(items))))
self.controller?.present(contextController, in: .window(.root))
})
return ContextController.Items(content: .list(items))
}
let contextController = ContextController(presentationData: self.presentationData, source: source, items: items)
self.controller?.present(contextController, in: .window(.root))
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
@ -1434,7 +1470,7 @@ public class BrowserScreen: ViewController, MinimizableController {
public enum Subject {
case webPage(url: String)
case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation)
case instantPage(webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation, preloadedResources: [Any]?)
case document(file: TelegramMediaFile, canShare: Bool)
case pdfDocument(file: TelegramMediaFile, canShare: Bool)
}

View File

@ -173,6 +173,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
private var presentationData: PresentationData
let webView: WebView
var readability: Readability?
private let errorView: ComponentHostView<Empty>
private var currentError: Error?
@ -191,7 +192,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
private let faviconDisposable = MetaDisposable()
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
var pushContent: (BrowserScreen.Subject, BrowserContent?) -> Void = { _, _ in }
var openAppUrl: (String) -> Void = { _ in }
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
var minimize: () -> Void = { }
@ -238,7 +239,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
contentController.add(WeakScriptMessageHandler { message in
handleScriptMessageImpl?(message)
}, name: "performAction")
configuration.userContentController = contentController
configuration.applicationNameForUserAgent = computedUserAgent()
}
@ -377,13 +378,36 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
self.webView.evaluateJavaScript(js) { _, _ in }
}
func toggleInstantView(_ enabled: Bool) {
if enabled {
if let instantPage = self.instantPage {
self.pushContent(.instantPage(webPage: instantPage, anchor: nil, sourceLocation: InstantPageSourceLocation(userLocation: .other, peerType: .channel), preloadedResources: self.instantPageResources), self)
} else if let readability = self.readability {
readability.webView.frame = self.webView.frame
self.addSubview(readability.webView)
var collapsedFrame = readability.webView.frame
collapsedFrame.size.height = 0.0
readability.webView.clipsToBounds = true
readability.webView.layer.animateFrame(from: collapsedFrame, to: readability.webView.frame, duration: 0.3)
}
} else if let readability = self.readability {
var collapsedFrame = readability.webView.frame
collapsedFrame.size.height = 0.0
readability.webView.layer.animateFrame(from: readability.webView.frame, to: collapsedFrame, duration: 0.3, removeOnCompletion: false, completion: { _ in
readability.webView.removeFromSuperview()
readability.webView.layer.removeAllAnimations()
})
}
}
private var didSetupSearch = false
private func setupSearch(completion: @escaping () -> Void) {
guard !self.didSetupSearch else {
completion()
return
}
let bundle = getAppBundle()
guard let scriptPath = bundle.path(forResource: "UIWebViewSearch", ofType: "js") else {
return
@ -764,6 +788,9 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
}
}
private var instantPage: TelegramMediaWebpage?
private var instantPageResources: [Any]?
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
if let _ = self.currentError {
self.currentError = nil
@ -772,6 +799,10 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
}
}
self.updateFontState(self.currentFontState, force: true)
self.readability = nil
self.instantPage = nil
self.instantPageResources = nil
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
@ -780,6 +811,49 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
.withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) })
}
self.parseFavicon()
guard let url = URL(string: self._state.url) else {
return
}
if #available(iOS 14.5, *) {
self.webView.createWebArchiveData { [weak self] result in
guard let self, case let .success(data) = result else {
return
}
let readability = Readability(url: url, archiveData: data, completionHandler: { [weak self] result, error in
guard let self else {
return
}
if let (webPage, resources) = result {
self.updateState {$0
.withUpdatedHasInstantView(true)
}
self.instantPage = webPage
self.instantPageResources = resources
let _ = (updatedRemoteWebpage(postbox: self.context.account.postbox, network: self.context.account.network, accountPeerId: self.context.account.peerId, webPage: WebpageReference(TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: self._state.url, displayUrl: "", hash: 0, type: nil, websiteName: nil, title: nil, text: nil, embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil)))))
|> deliverOnMainQueue).start(next: { [weak self] webPage in
guard let self, let webPage, case let .Loaded(result) = webPage.content, let _ = result.instantPage else {
return
}
// let _ = self
// #if DEBUG
//
// #else
self.instantPage = webPage
// #endif
})
} else {
self.instantPage = nil
self.instantPageResources = nil
self.updateState {$0
.withUpdatedHasInstantView(false)
}
}
})
self.readability = readability
}
}
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
@ -951,7 +1025,7 @@ final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKU
navigationController.pushViewController(controller)
return (controller.node.content.last as? BrowserWebContent)?.webView
} else {
self.pushContent(subject)
self.pushContent(subject, nil)
}
return nil
}

View File

@ -26,6 +26,7 @@ swift_library(
"//submodules/LocationResources:LocationResources",
"//submodules/UndoUI:UndoUI",
"//submodules/TranslateUI:TranslateUI",
"//submodules/Tuples:Tuples",
],
visibility = [
"//visibility:public",

View File

@ -27,7 +27,7 @@ public final class InstantPageAnchorItem: InstantPageItem {
public func drawInTile(context: CGContext) {
}
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
return nil
}

View File

@ -36,7 +36,7 @@ public final class InstantPageArticleItem: InstantPageItem {
self.hasRTL = hasRTL
}
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
return InstantPageArticleNode(context: context, item: self, webPage: self.webPage, strings: strings, theme: theme, contentItems: self.contentItems, contentSize: self.contentSize, cover: self.cover, url: self.url, webpageId: self.webpageId, openUrl: openUrl)
}

View File

@ -23,7 +23,7 @@ public final class InstantPageAudioItem: InstantPageItem {
self.medias = [media]
}
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
return InstantPageAudioNode(context: context, strings: strings, theme: theme, webPage: self.webpage, media: self.media, openMedia: openMedia)
}

View File

@ -22,6 +22,7 @@ public final class InstantPageContentNode : ASDisplayNode {
private let openUrl: (InstantPageUrlItem) -> Void
private let activatePinchPreview: ((PinchSourceContainerNode) -> Void)?
private let pinchPreviewFinished: ((InstantPageNode) -> Void)?
private let getPreloadedResource: (String) -> Data?
var currentLayoutTiles: [InstantPageTile] = []
var currentLayoutItemsWithNodes: [InstantPageItem] = []
@ -42,7 +43,7 @@ public final class InstantPageContentNode : ASDisplayNode {
private var previousVisibleBounds: CGRect?
init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize, inOverlayPanel: Bool = false, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void) {
init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, items: [InstantPageItem], contentSize: CGSize, inOverlayPanel: Bool = false, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, getPreloadedResource: @escaping (String) -> Data?) {
self.context = context
self.strings = strings
self.nameDisplayOrder = nameDisplayOrder
@ -55,6 +56,7 @@ public final class InstantPageContentNode : ASDisplayNode {
self.pinchPreviewFinished = pinchPreviewFinished
self.openPeer = openPeer
self.openUrl = openUrl
self.getPreloadedResource = getPreloadedResource
self.currentLayout = InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
self.contentSize = contentSize
@ -210,7 +212,7 @@ public final class InstantPageContentNode : ASDisplayNode {
}, updateWebEmbedHeight: { _ in
}, updateDetailsExpanded: { [weak self] expanded in
self?.updateDetailsExpanded(detailsIndex, expanded)
}, currentExpandedDetails: self.currentExpandedDetails) {
}, currentExpandedDetails: self.currentExpandedDetails, getPreloadedResource: self.getPreloadedResource) {
newNode.frame = itemFrame
newNode.updateLayout(size: itemFrame.size, transition: transition)
if let topNode = topNode {

View File

@ -649,7 +649,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
self?.updateWebEmbedHeight(embedIndex, height)
}, updateDetailsExpanded: { [weak self] expanded in
self?.updateDetailsExpanded(detailsIndex, expanded)
}, currentExpandedDetails: self.currentExpandedDetails) {
}, currentExpandedDetails: self.currentExpandedDetails, getPreloadedResource: { _ in return nil }) {
newNode.frame = itemFrame
newNode.updateLayout(size: itemFrame.size, transition: transition)
if let topNode = topNode {

View File

@ -39,12 +39,12 @@ public final class InstantPageDetailsItem: InstantPageItem {
self.index = index
}
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
var expanded: Bool?
if let expandedDetails = currentExpandedDetails, let currentlyExpanded = expandedDetails[self.index] {
expanded = currentlyExpanded
}
return InstantPageDetailsNode(context: context, sourceLocation: sourceLocation, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, item: self, openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview, pinchPreviewFinished: pinchPreviewFinished, openPeer: openPeer, openUrl: openUrl, currentlyExpanded: expanded, updateDetailsExpanded: updateDetailsExpanded)
return InstantPageDetailsNode(context: context, sourceLocation: sourceLocation, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, item: self, openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview, pinchPreviewFinished: pinchPreviewFinished, openPeer: openPeer, openUrl: openUrl, currentlyExpanded: expanded, updateDetailsExpanded: updateDetailsExpanded, getPreloadedResource: getPreloadedResource)
}
public func matchesAnchor(_ anchor: String) -> Bool {

View File

@ -35,7 +35,7 @@ public final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
public var requestLayoutUpdate: ((Bool) -> Void)?
init(context: AccountContext, sourceLocation: InstantPageSourceLocation, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, item: InstantPageDetailsItem, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, currentlyExpanded: Bool?, updateDetailsExpanded: @escaping (Bool) -> Void) {
init(context: AccountContext, sourceLocation: InstantPageSourceLocation, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, item: InstantPageDetailsItem, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, currentlyExpanded: Bool?, updateDetailsExpanded: @escaping (Bool) -> Void, getPreloadedResource: @escaping (String) -> Data?) {
self.context = context
self.strings = strings
self.nameDisplayOrder = nameDisplayOrder
@ -65,7 +65,7 @@ public final class InstantPageDetailsNode: ASDisplayNode, InstantPageNode {
self.arrowNode = InstantPageDetailsArrowNode(color: theme.controlColor, open: self.expanded)
self.separatorNode = ASDisplayNode()
self.contentNode = InstantPageContentNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, sourceLocation: sourceLocation, theme: theme, items: item.items, contentSize: CGSize(width: item.frame.width, height: item.frame.height - item.titleHeight), openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview, pinchPreviewFinished: pinchPreviewFinished, openPeer: openPeer, openUrl: openUrl)
self.contentNode = InstantPageContentNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, sourceLocation: sourceLocation, theme: theme, items: item.items, contentSize: CGSize(width: item.frame.width, height: item.frame.height - item.titleHeight), openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview, pinchPreviewFinished: pinchPreviewFinished, openPeer: openPeer, openUrl: openUrl, getPreloadedResource: getPreloadedResource)
super.init()

View File

@ -0,0 +1,48 @@
import Foundation
import Postbox
import TelegramCore
import PersistentStringHash
public struct InstantPageExternalMediaResourceId {
public let url: String
public var uniqueId: String {
return "instantpage-media-\(persistentHash32(self.url))"
}
public var hashValue: Int {
return self.uniqueId.hashValue
}
}
public class InstantPageExternalMediaResource: TelegramMediaResource {
public let url: String
public var size: Int64? {
return nil
}
public init(url: String) {
self.url = url
}
public required init(decoder: PostboxDecoder) {
self.url = decoder.decodeStringForKey("u", orElse: "")
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeString(self.url, forKey: "u")
}
public var id: MediaResourceId {
return MediaResourceId(InstantPageExternalMediaResourceId(url: self.url).uniqueId)
}
public func isEqual(to: MediaResource) -> Bool {
if let to = to as? InstantPageExternalMediaResource {
return self.url == to.url
} else {
return false
}
}
}

View File

@ -20,7 +20,7 @@ public final class InstantPageFeedbackItem: InstantPageItem {
self.webPage = webPage
}
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
return InstantPageFeedbackNode(context: context, strings: strings, theme: theme, webPage: self.webPage, openUrl: openUrl)
}

View File

@ -44,8 +44,8 @@ public final class InstantPageImageItem: InstantPageItem {
self.fit = fit
}
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
return InstantPageImageNode(context: context, sourceLocation: sourceLocation, theme: theme, webPage: self.webPage, media: self.media, attributes: self.attributes, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview, pinchPreviewFinished: pinchPreviewFinished)
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
return InstantPageImageNode(context: context, sourceLocation: sourceLocation, theme: theme, webPage: self.webPage, media: self.media, attributes: self.attributes, interactive: self.interactive, roundCorners: self.roundCorners, fit: self.fit, openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview, pinchPreviewFinished: pinchPreviewFinished, getPreloadedResource: getPreloadedResource)
}
public func matchesAnchor(_ anchor: String) -> Bool {

View File

@ -14,6 +14,7 @@ import LiveLocationPositionNode
import AppBundle
import TelegramUIPreferences
import ContextUI
import Tuples
private struct FetchControls {
let fetch: (Bool) -> Void
@ -48,7 +49,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
private var themeUpdated: Bool = false
init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?) {
init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute], interactive: Bool, roundCorners: Bool, fit: Bool, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, getPreloadedResource: @escaping (String) -> Data?) {
self.context = context
self.theme = theme
self.webPage = webPage
@ -72,51 +73,103 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.addSubnode(self.pinchContainerNode)
if case let .image(image) = media.media, let largest = largestImageRepresentation(image.representations) {
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: image) {
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start())
}
self.fetchControls = FetchControls(fetch: { [weak self] manual in
if let strongSelf = self {
strongSelf.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start())
if let externalResource = largest.resource as? InstantPageExternalMediaResource {
var url = externalResource.url
if !url.hasPrefix("http") && !url.hasPrefix("https") {
url = "https:\(url)"
}
}, cancel: {
chatMessagePhotoCancelInteractiveFetch(account: context.account, photoReference: imageReference)
})
if interactive {
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
displayLinkDispatcher.dispatch {
if let strongSelf = self {
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
strongSelf.updateFetchStatus()
let photoData: Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError>
if let preloadedData = getPreloadedResource(externalResource.url) {
photoData = .single(Tuple4(nil, preloadedData, .full, true))
} else {
photoData = context.engine.resources.httpData(url: url, preserveExactUrl: true)
|> map(Optional.init)
|> `catch` { _ -> Signal<Data?, NoError> in
return .single(nil)
}
|> map { data in
if let data {
return Tuple4(nil, data, .full, true)
} else {
return Tuple4(nil, nil, .full, false)
}
}
}))
if media.url != nil {
self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink")
self.pinchContainerNode.contentNode.addSubnode(self.linkIconNode)
}
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
self.imageNode.setSignal(chatMessagePhotoInternal(photoData: photoData)
|> map { _, _, generate in
return generate
})
} else {
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: image) {
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start())
}
self.fetchControls = FetchControls(fetch: { [weak self] manual in
if let strongSelf = self {
strongSelf.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start())
}
}, cancel: {
chatMessagePhotoCancelInteractiveFetch(account: context.account, photoReference: imageReference)
})
if interactive {
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
displayLinkDispatcher.dispatch {
if let strongSelf = self {
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
strongSelf.updateFetchStatus()
}
}
}))
if media.url != nil {
self.linkIconNode.image = UIImage(bundleImageName: "Instant View/ImageLink")
self.pinchContainerNode.contentNode.addSubnode(self.linkIconNode)
}
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
}
}
} else if case let .file(file) = media.media {
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
if file.mimeType.hasPrefix("image/") {
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: file) {
_ = freeMediaFileInteractiveFetched(account: context.account, userLocation: sourceLocation.userLocation, fileReference: fileReference).start()
if let externalResource = file.resource as? InstantPageExternalMediaResource {
let photoData: Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError>
if let preloadedData = getPreloadedResource(externalResource.url) {
photoData = .single(Tuple4(nil, preloadedData, .full, true))
} else {
photoData = context.engine.resources.httpData(url: externalResource.url, preserveExactUrl: true)
|> map(Optional.init)
|> `catch` { _ -> Signal<Data?, NoError> in
return .single(nil)
}
|> map { data in
if let data {
return Tuple4(nil, data, .full, true)
} else {
return Tuple4(nil, nil, .full, false)
}
}
}
self.imageNode.setSignal(instantPageImageFile(account: context.account, userLocation: sourceLocation.userLocation, fileReference: fileReference, fetched: true))
self.imageNode.setSignal(chatMessagePhotoInternal(photoData: photoData)
|> map { _, _, generate in
return generate
})
} else {
self.imageNode.setSignal(chatMessageVideo(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, videoReference: fileReference))
}
if file.isVideo {
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
if file.mimeType.hasPrefix("image/") {
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: file) {
_ = freeMediaFileInteractiveFetched(account: context.account, userLocation: sourceLocation.userLocation, fileReference: fileReference).start()
}
self.imageNode.setSignal(instantPageImageFile(account: context.account, userLocation: sourceLocation.userLocation, fileReference: fileReference, fetched: true))
} else {
self.imageNode.setSignal(chatMessageVideo(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, videoReference: fileReference))
}
if file.isVideo {
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
}
}
} else if case let .geo(map) = media.media {
self.addSubnode(self.pinNode)

View File

@ -15,7 +15,7 @@ public protocol InstantPageItem {
func matchesAnchor(_ anchor: String) -> Bool
func drawInTile(context: CGContext)
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode?
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode?
func matchesNode(_ node: InstantPageNode) -> Bool
func linkSelectionRects(at point: CGPoint) -> [CGRect]

View File

@ -884,9 +884,11 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, userLoc
let closingSpacing = spacingBetweenBlocks(upper: previousBlock, lower: nil)
contentSize.height += closingSpacing
let feedbackItem = InstantPageFeedbackItem(frame: CGRect(x: 0.0, y: contentSize.height, width: boundingWidth, height: 40.0), webPage: webPage)
contentSize.height += feedbackItem.frame.height
items.append(feedbackItem)
if webPage.webpageId.id != 0 {
let feedbackItem = InstantPageFeedbackItem(frame: CGRect(x: 0.0, y: contentSize.height, width: boundingWidth, height: 40.0), webPage: webPage)
contentSize.height += feedbackItem.frame.height
items.append(feedbackItem)
}
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
}

View File

@ -26,7 +26,7 @@ public final class InstantPagePeerReferenceItem: InstantPageItem {
self.rtl = rtl
}
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
return InstantPagePeerReferenceNode(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, initialPeer: self.initialPeer, safeInset: self.safeInset, transparent: self.transparent, rtl: self.rtl, openPeer: openPeer)
}

View File

@ -28,7 +28,7 @@ public final class InstantPagePlayableVideoItem: InstantPageItem {
self.interactive = interactive
}
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
return InstantPagePlayableVideoNode(context: context, userLocation: sourceLocation.userLocation, webPage: self.webPage, theme: theme, media: self.media, interactive: self.interactive, openMedia: openMedia)
}

View File

@ -203,7 +203,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, ASScrollVie
let sideInset: CGFloat = 16.0
let (_, items, contentSize) = layoutTextItemWithString(self.anchorText, boundingWidth: width - sideInset * 2.0, offset: CGPoint(x: sideInset, y: sideInset), media: media, webpage: self.webPage)
let contentNode = InstantPageContentNode(context: self.context, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, sourceLocation: self.sourceLocation, theme: self.theme, items: items, contentSize: CGSize(width: width, height: contentSize.height), inOverlayPanel: true, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { _ in }, openUrl: { _ in })
let contentNode = InstantPageContentNode(context: self.context, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, sourceLocation: self.sourceLocation, theme: self.theme, items: items, contentSize: CGSize(width: width, height: contentSize.height), inOverlayPanel: true, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { _ in }, openUrl: { _ in }, getPreloadedResource: { url in return nil})
transition.updateFrame(node: contentNode, frame: CGRect(origin: CGPoint(x: 0.0, y: titleAreaHeight), size: CGSize(width: width, height: contentSize.height)))
self.contentContainerNode.insertSubnode(contentNode, at: 0)
self.contentNode = contentNode

View File

@ -61,7 +61,7 @@ public final class InstantPageShapeItem: InstantPageItem {
return false
}
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
return nil
}

View File

@ -20,7 +20,7 @@ final class InstantPageSlideshowItem: InstantPageItem {
self.medias = medias
}
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
return InstantPageSlideshowNode(context: context, sourceLocation: sourceLocation, theme: theme, webPage: webPage, medias: self.medias, openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview, pinchPreviewFinished: pinchPreviewFinished)
}

View File

@ -187,7 +187,7 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, ASScrollViewDe
let media = self.items[index]
let contentNode: ASDisplayNode
if case .image = media.media {
contentNode = InstantPageImageNode(context: self.context, sourceLocation: self.sourceLocation, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia, activatePinchPreview: self.activatePinchPreview, pinchPreviewFinished: self.pinchPreviewFinished)
contentNode = InstantPageImageNode(context: self.context, sourceLocation: self.sourceLocation, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia, activatePinchPreview: self.activatePinchPreview, pinchPreviewFinished: self.pinchPreviewFinished, getPreloadedResource: { _ in return nil })
} else if case .file = media.media {
contentNode = ASDisplayNode()
} else {

View File

@ -198,7 +198,7 @@ final class InstantPageSubContentNode : ASDisplayNode {
}, updateWebEmbedHeight: { _ in
}, updateDetailsExpanded: { [weak self] expanded in
self?.updateDetailsExpanded(detailsIndex, expanded)
}, currentExpandedDetails: self.currentExpandedDetails) {
}, currentExpandedDetails: self.currentExpandedDetails, getPreloadedResource: { _ in return nil }) {
newNode.frame = itemFrame
newNode.updateLayout(size: itemFrame.size, transition: transition)
if let topNode = topNode {

View File

@ -199,12 +199,12 @@ public final class InstantPageTableItem: InstantPageScrollableItem {
return false
}
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
var additionalNodes: [InstantPageNode] = []
for cell in self.cells {
for item in cell.additionalItems {
if item.wantsNode {
if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourceLocation: sourceLocation, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) {
if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourceLocation: sourceLocation, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil, getPreloadedResource: getPreloadedResource) {
node.frame = item.frame.offsetBy(dx: cell.frame.minX, dy: cell.frame.minY)
additionalNodes.append(node)
}

View File

@ -435,7 +435,7 @@ public final class InstantPageTextItem: InstantPageItem {
return false
}
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
return nil
}
@ -484,11 +484,11 @@ final class InstantPageScrollableTextItem: InstantPageScrollableItem {
context.restoreGState()
}
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
var additionalNodes: [InstantPageNode] = []
for item in additionalItems {
if item.wantsNode {
if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourceLocation: sourceLocation, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil) {
if let node = item.node(context: context, strings: strings, nameDisplayOrder: nameDisplayOrder, theme: theme, sourceLocation: sourceLocation, openMedia: { _ in }, longPressMedia: { _ in }, activatePinchPreview: nil, pinchPreviewFinished: nil, openPeer: { _ in }, openUrl: { _ in}, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { _ in }, currentExpandedDetails: nil, getPreloadedResource: getPreloadedResource) {
node.frame = item.frame
additionalNodes.append(node)
}

View File

@ -100,12 +100,12 @@ final class InstantPageTextStyleStack {
}
case .subscript:
if baselineOffset == nil {
baselineOffset = 0.35
baselineOffset = -0.35
underline = false
}
case .superscript:
if baselineOffset == nil {
baselineOffset = -0.35
baselineOffset = 0.35
}
case let .markerColor(color):
if markerColor == nil {

View File

@ -24,7 +24,7 @@ public final class InstantPageWebEmbedItem: InstantPageItem {
self.enableScrolling = enableScrolling
}
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?) -> InstantPageNode? {
public func node(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, sourceLocation: InstantPageSourceLocation, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, openPeer: @escaping (EnginePeer) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, currentExpandedDetails: [Int : Bool]?, getPreloadedResource: @escaping (String) -> Data?) -> InstantPageNode? {
return InstantPageWebEmbedNode(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling, updateWebEmbedHeight: updateWebEmbedHeight)
}

View File

@ -81,11 +81,21 @@ public func presentGiveawayInfoController(
onlyNewSubscribers = true
}
let author = message.forwardInfo?.author ?? message.author?._asPeer()
var author = message.forwardInfo?.author ?? message.author?._asPeer()
if author is TelegramChannel {
} else {
if let peer = message.forwardInfo?.source ?? message.peers[message.id.peerId] {
author = peer
}
}
var isGroup = false
if let channel = author as? TelegramChannel, case .group = channel.info {
isGroup = true
}
var peerName = ""
if let author {
peerName = EnginePeer(author).compactDisplayTitle
}
var groupsAndChannels = false
var channelsCount: Int32 = 1
@ -113,10 +123,7 @@ public func presentGiveawayInfoController(
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var peerName = ""
if let channel = author as? TelegramChannel {
peerName = EnginePeer(channel).compactDisplayTitle
}
let timeZone = TimeZone.current
let untilDate = stringForDate(timestamp: untilDateValue, timeZone: timeZone, strings: presentationData.strings)

View File

@ -3,8 +3,12 @@ import Postbox
import SwiftSignalKit
import MtProtoKit
public func fetchHttpResource(url: String) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
if let urlString = url.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed), let url = URL(string: urlString) {
public func fetchHttpResource(url: String, preserveExactUrl: Bool = false) -> Signal<MediaResourceDataFetchResult, MediaResourceDataFetchError> {
var urlString: String? = url
if !preserveExactUrl {
urlString = url.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)
}
if let urlString, let url = URL(string: urlString) {
let signal = MTHttpRequestOperation.data(forHttpUrl: url)!
return Signal { subscriber in
subscriber.putNext(.reset)

View File

@ -341,8 +341,8 @@ public extension TelegramEngine {
}
}
public func httpData(url: String) -> Signal<Data, EngineMediaResource.Fetch.Error> {
return fetchHttpResource(url: url)
public func httpData(url: String, preserveExactUrl: Bool = false) -> Signal<Data, EngineMediaResource.Fetch.Error> {
return fetchHttpResource(url: url, preserveExactUrl: preserveExactUrl)
|> mapError { _ -> EngineMediaResource.Fetch.Error in
return .generic
}

View File

@ -262,7 +262,7 @@ public func actualizedWebpage(account: Account, webpage: TelegramMediaWebpage) -
}
}
func updatedRemoteWebpage(postbox: Postbox, network: Network, accountPeerId: EnginePeer.Id, webPage: WebpageReference) -> Signal<TelegramMediaWebpage?, NoError> {
public func updatedRemoteWebpage(postbox: Postbox, network: Network, accountPeerId: EnginePeer.Id, webPage: WebpageReference) -> Signal<TelegramMediaWebpage?, NoError> {
if case let .webPage(id, url) = webPage.content {
return network.request(Api.functions.messages.getWebPage(url: url, hash: 0))
|> map(Optional.init)
@ -270,14 +270,20 @@ func updatedRemoteWebpage(postbox: Postbox, network: Network, accountPeerId: Eng
return .single(nil)
}
|> mapToSignal { result -> Signal<TelegramMediaWebpage?, NoError> in
if let result = result, case let .webPage(webpage, chats, users) = result, let updatedWebpage = telegramMediaWebpageFromApiWebpage(webpage), case .Loaded = updatedWebpage.content, updatedWebpage.webpageId.id == id {
return postbox.transaction { transaction -> TelegramMediaWebpage? in
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
if transaction.getMedia(updatedWebpage.webpageId) != nil {
updateMessageMedia(transaction: transaction, id: updatedWebpage.webpageId, media: updatedWebpage)
if let result = result, case let .webPage(webpage, chats, users) = result, let updatedWebpage = telegramMediaWebpageFromApiWebpage(webpage), case .Loaded = updatedWebpage.content {
if updatedWebpage.webpageId.id == id {
return postbox.transaction { transaction -> TelegramMediaWebpage? in
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: parsedPeers)
if transaction.getMedia(updatedWebpage.webpageId) != nil {
updateMessageMedia(transaction: transaction, id: updatedWebpage.webpageId, media: updatedWebpage)
}
return updatedWebpage
}
return updatedWebpage
} else if id == 0 {
return .single(updatedWebpage)
} else {
return .single(nil)
}
} else {
return .single(nil)

View File

@ -79,6 +79,11 @@ private final class NavigationContainer: UIView, UIGestureRecognizerDelegate {
}
public final class NavigationStackComponent<ChildEnvironment: Equatable>: Component {
public enum CurlTransition {
case show
case hide
}
public let items: [AnyComponentWithIdentity<ChildEnvironment>]
public let requestPop: () -> Void
@ -105,7 +110,7 @@ public final class NavigationStackComponent<ChildEnvironment: Equatable>: Compon
super.init(frame: frame)
self.dimView.alpha = 0.0
self.dimView.backgroundColor = UIColor.black.withAlphaComponent(0.2)
self.dimView.backgroundColor = UIColor.black.withAlphaComponent(0.3)
self.dimView.isUserInteractionEnabled = false
self.addSubview(self.dimView)
}
@ -166,11 +171,20 @@ public final class NavigationStackComponent<ChildEnvironment: Equatable>: Compon
self.component = component
self.state = state
var transition = transition
var curlTransition: NavigationStackComponent<ChildEnvironment>.CurlTransition?
if let curlTransitionValue = transition.userData(NavigationStackComponent<ChildEnvironment>.CurlTransition.self) {
transition = .immediate
curlTransition = curlTransitionValue
}
let navigationTransitionFraction = self.navigationContainer.transitionFraction
self.navigationContainer.isNavigationEnabled = component.items.count > 1
var validItemIds: [AnyHashable] = []
var removeImpl: (() -> Void)?
var readyItems: [ReadyItem] = []
for i in 0 ..< component.items.count {
let item = component.items[i]
@ -184,6 +198,7 @@ public final class NavigationStackComponent<ChildEnvironment: Equatable>: Compon
} else {
itemTransition = itemTransition.withAnimation(.none)
itemView = ItemView()
itemView.clipsToBounds = true
self.itemViews[itemId] = itemView
itemView.contents.parentState = state
}
@ -242,7 +257,18 @@ public final class NavigationStackComponent<ChildEnvironment: Equatable>: Compon
readyItem.itemTransition.setFrame(view: readyItem.itemView.dimView, frame: CGRect(origin: .zero, size: availableSize))
readyItem.itemTransition.setAlpha(view: readyItem.itemView.dimView, alpha: 1.0 - alphaTransitionFraction)
if readyItem.index > 0 && isAdded {
if curlTransition == .show && isAdded {
var fromFrame = itemFrame
fromFrame.size.height = 0.0
let transition = ComponentTransition.easeInOut(duration: 0.3)
transition.animateBoundsSize(view: readyItem.itemView, from: fromFrame.size, to: itemFrame.size, completion: { _ in
removeImpl?()
})
transition.animatePosition(view: readyItem.itemView, from: fromFrame.center, to: itemFrame.center)
} else if curlTransition == .hide && isAdded {
let transition = ComponentTransition.easeInOut(duration: 0.3)
transition.animateAlpha(view: readyItem.itemView.dimView, from: 1.0, to: 0.0)
} else if readyItem.index > 0 && isAdded {
transition.animatePosition(view: itemComponentView, from: CGPoint(x: itemFrame.width, y: 0.0), to: .zero, additive: true, completion: nil)
}
}
@ -263,23 +289,49 @@ public final class NavigationStackComponent<ChildEnvironment: Equatable>: Compon
removedItemIds.append(id)
}
}
for id in removedItemIds {
guard let itemView = self.itemViews[id] else {
continue
}
if let itemComponeentView = itemView.contents.view {
var position = itemComponeentView.center
position.x += itemComponeentView.bounds.width
transition.setPosition(view: itemComponeentView, position: position, completion: { _ in
removeImpl = {
for id in removedItemIds {
guard let itemView = self.itemViews[id] else {
continue
}
if let itemComponentView = itemView.contents.view, curlTransition != .show {
if curlTransition == .hide {
itemView.superview?.bringSubviewToFront(itemView)
var toFrame = itemView.frame
toFrame.size.height = 0.0
let transition = ComponentTransition.easeInOut(duration: 0.3)
transition.setFrame(view: itemView, frame: toFrame, completion: { _ in
itemView.removeFromSuperview()
self.itemViews.removeValue(forKey: id)
})
} else {
var position = itemComponentView.center
position.x += itemComponentView.bounds.width
transition.setPosition(view: itemComponentView, position: position, completion: { _ in
itemView.removeFromSuperview()
self.itemViews.removeValue(forKey: id)
})
}
} else {
itemView.removeFromSuperview()
self.itemViews.removeValue(forKey: id)
})
} else {
itemView.removeFromSuperview()
self.itemViews.removeValue(forKey: id)
}
}
}
if curlTransition == .show {
let transition = ComponentTransition.easeInOut(duration: 0.3)
for id in removedItemIds {
guard let itemView = self.itemViews[id] else {
continue
}
transition.setAlpha(view: itemView.dimView, alpha: 1.0)
}
} else {
removeImpl?()
}
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
self.navigationContainer.frame = CGRect(origin: .zero, size: contentSize)

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -402,7 +402,7 @@ func makeInstantPageControllerImpl(context: AccountContext, message: Message, so
}
func makeInstantPageControllerImpl(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController {
return BrowserScreen(context: context, subject: .instantPage(webPage: webPage, anchor: anchor, sourceLocation: sourceLocation))
return BrowserScreen(context: context, subject: .instantPage(webPage: webPage, anchor: anchor, sourceLocation: sourceLocation, preloadedResources: nil))
}
func openChatWallpaperImpl(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void) {