mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
611 lines
26 KiB
Swift
611 lines
26 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import ComponentFlow
|
|
import TelegramCore
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import PresentationDataUtils
|
|
import AccountContext
|
|
import WebKit
|
|
import AppBundle
|
|
import PromptUI
|
|
import SafariServices
|
|
import ShareController
|
|
import UndoUI
|
|
|
|
private final class IpfsSchemeHandler: NSObject, WKURLSchemeHandler {
|
|
private final class PendingTask {
|
|
let sourceTask: any WKURLSchemeTask
|
|
var urlSessionTask: URLSessionTask?
|
|
let isCompleted = Atomic<Bool>(value: false)
|
|
|
|
init(sourceTask: any WKURLSchemeTask) {
|
|
self.sourceTask = sourceTask
|
|
|
|
var cleanUrl = sourceTask.request.url!.absoluteString
|
|
if let range = cleanUrl.range(of: "/ipfs/") {
|
|
cleanUrl = "ipfs://" + String(cleanUrl[range.upperBound...])
|
|
} else if let range = cleanUrl.range(of: "/ipns/") {
|
|
cleanUrl = "ipns://" + String(cleanUrl[range.upperBound...])
|
|
}
|
|
print("Load: \(cleanUrl)")
|
|
cleanUrl = cleanUrl.replacingOccurrences(of: "ipns://", with: "ipns/")
|
|
cleanUrl = cleanUrl.replacingOccurrences(of: "ipfs://", with: "ipfs/")
|
|
let mappedUrl = "https://cloudflare-ipfs.com/\(cleanUrl)"
|
|
let isCompleted = self.isCompleted
|
|
self.urlSessionTask = URLSession.shared.dataTask(with: URLRequest(url: URL(string: mappedUrl)!), completionHandler: { data, response, error in
|
|
if isCompleted.swap(true) {
|
|
return
|
|
}
|
|
|
|
if let error {
|
|
sourceTask.didFailWithError(error)
|
|
} else {
|
|
if let response {
|
|
sourceTask.didReceive(response)
|
|
}
|
|
if let data {
|
|
sourceTask.didReceive(data)
|
|
}
|
|
sourceTask.didFinish()
|
|
}
|
|
})
|
|
self.urlSessionTask?.resume()
|
|
}
|
|
|
|
func cancel() {
|
|
if let urlSessionTask = self.urlSessionTask {
|
|
self.urlSessionTask = nil
|
|
if !self.isCompleted.swap(true) {
|
|
switch urlSessionTask.state {
|
|
case .running, .suspended:
|
|
urlSessionTask.cancel()
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var pendingTasks: [PendingTask] = []
|
|
|
|
func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) {
|
|
self.pendingTasks.append(PendingTask(sourceTask: urlSchemeTask))
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) {
|
|
if let index = self.pendingTasks.firstIndex(where: { $0.sourceTask === urlSchemeTask }) {
|
|
let task = self.pendingTasks[index]
|
|
self.pendingTasks.remove(at: index)
|
|
task.cancel()
|
|
}
|
|
}
|
|
}
|
|
|
|
final class BrowserWebContent: UIView, BrowserContent, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate {
|
|
private let context: AccountContext
|
|
|
|
private let webView: WKWebView
|
|
|
|
let uuid: UUID
|
|
|
|
private var _state: BrowserContentState
|
|
private let statePromise: Promise<BrowserContentState>
|
|
|
|
var currentState: BrowserContentState {
|
|
return self._state
|
|
}
|
|
var state: Signal<BrowserContentState, NoError> {
|
|
return self.statePromise.get()
|
|
}
|
|
|
|
private let faviconDisposable = MetaDisposable()
|
|
|
|
var pushContent: (BrowserScreen.Subject) -> Void = { _ in }
|
|
var onScrollingUpdate: (ContentScrollingUpdate) -> Void = { _ in }
|
|
var minimize: () -> Void = { }
|
|
var present: (ViewController, Any?) -> Void = { _, _ in }
|
|
var presentInGlobalOverlay: (ViewController) -> Void = { _ in }
|
|
var getNavigationController: () -> NavigationController? = { return nil }
|
|
|
|
init(context: AccountContext, url: String) {
|
|
self.context = context
|
|
self.uuid = UUID()
|
|
|
|
let configuration = WKWebViewConfiguration()
|
|
|
|
if context.sharedContext.immediateExperimentalUISettings.browserExperiment {
|
|
configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ipns")
|
|
configuration.setURLSchemeHandler(IpfsSchemeHandler(), forURLScheme: "ipfs")
|
|
}
|
|
|
|
self.webView = WKWebView(frame: CGRect(), configuration: configuration)
|
|
self.webView.allowsLinkPreview = true
|
|
|
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
|
self.webView.scrollView.contentInsetAdjustmentBehavior = .never
|
|
}
|
|
|
|
var title: String = ""
|
|
if let parsedUrl = URL(string: url) {
|
|
let request = URLRequest(url: parsedUrl)
|
|
self.webView.load(request)
|
|
|
|
title = parsedUrl.host ?? ""
|
|
}
|
|
|
|
self._state = BrowserContentState(title: title, url: url, estimatedProgress: 0.0, readingProgress: 0.0, contentType: .webPage)
|
|
self.statePromise = Promise<BrowserContentState>(self._state)
|
|
|
|
super.init(frame: .zero)
|
|
|
|
self.webView.allowsBackForwardNavigationGestures = true
|
|
self.webView.scrollView.delegate = self
|
|
self.webView.navigationDelegate = self
|
|
self.webView.uiDelegate = self
|
|
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.title), options: [], context: nil)
|
|
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: [], context: nil)
|
|
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil)
|
|
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack), options: [], context: nil)
|
|
self.webView.addObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward), options: [], context: nil)
|
|
|
|
self.addSubview(self.webView)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.title))
|
|
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.url))
|
|
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
|
|
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack))
|
|
self.webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward))
|
|
|
|
self.faviconDisposable.dispose()
|
|
}
|
|
|
|
func setFontSize(_ fontSize: CGFloat) {
|
|
let js = "document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust='\(Int(fontSize * 100.0))%'"
|
|
self.webView.evaluateJavaScript(js, completionHandler: nil)
|
|
}
|
|
|
|
func setForceSerif(_ force: Bool) {
|
|
let js: String
|
|
if force {
|
|
js = "document.getElementsByTagName(\'body\')[0].style.fontFamily = 'Georgia, serif';"
|
|
} else {
|
|
js = "document.getElementsByTagName(\'body\')[0].style.fontFamily = '\"Lucida Grande\", \"Lucida Sans Unicode\", Arial, Helvetica, Verdana, sans-serif';"
|
|
}
|
|
self.webView.evaluateJavaScript(js) { _, _ in
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
guard let scriptData = try? Data(contentsOf: URL(fileURLWithPath: scriptPath)) else {
|
|
return
|
|
}
|
|
guard let script = String(data: scriptData, encoding: .utf8) else {
|
|
return
|
|
}
|
|
self.didSetupSearch = true
|
|
self.webView.evaluateJavaScript(script, completionHandler: { _, error in
|
|
if error != nil {
|
|
print()
|
|
}
|
|
completion()
|
|
})
|
|
}
|
|
|
|
private var previousQuery: String?
|
|
func setSearch(_ query: String?, completion: ((Int) -> Void)?) {
|
|
guard self.previousQuery != query else {
|
|
return
|
|
}
|
|
self.previousQuery = query
|
|
self.setupSearch { [weak self] in
|
|
if let query = query {
|
|
let js = "uiWebview_HighlightAllOccurencesOfString('\(query)')"
|
|
self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] _, _ in
|
|
let js = "uiWebview_SearchResultCount"
|
|
self?.webView.evaluateJavaScript(js, completionHandler: { [weak self] result, _ in
|
|
if let result = result as? NSNumber {
|
|
self?.searchResultsCount = result.intValue
|
|
completion?(result.intValue)
|
|
} else {
|
|
completion?(0)
|
|
}
|
|
})
|
|
})
|
|
} else {
|
|
let js = "uiWebview_RemoveAllHighlights()"
|
|
self?.webView.evaluateJavaScript(js, completionHandler: nil)
|
|
|
|
self?.currentSearchResult = 0
|
|
self?.searchResultsCount = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
private var currentSearchResult: Int = 0
|
|
private var searchResultsCount: Int = 0
|
|
|
|
func scrollToPreviousSearchResult(completion: ((Int, Int) -> Void)?) {
|
|
let searchResultsCount = self.searchResultsCount
|
|
var index = self.currentSearchResult - 1
|
|
if index < 0 {
|
|
index = searchResultsCount - 1
|
|
}
|
|
self.currentSearchResult = index
|
|
|
|
let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')"
|
|
self.webView.evaluateJavaScript(js, completionHandler: { _, _ in
|
|
completion?(index, searchResultsCount)
|
|
})
|
|
}
|
|
|
|
func scrollToNextSearchResult(completion: ((Int, Int) -> Void)?) {
|
|
let searchResultsCount = self.searchResultsCount
|
|
var index = self.currentSearchResult + 1
|
|
if index >= searchResultsCount {
|
|
index = 0
|
|
}
|
|
self.currentSearchResult = index
|
|
|
|
let js = "uiWebview_ScrollTo('\(searchResultsCount - index - 1)')"
|
|
self.webView.evaluateJavaScript(js, completionHandler: { _, _ in
|
|
completion?(index, searchResultsCount)
|
|
})
|
|
}
|
|
|
|
func stop() {
|
|
self.webView.stopLoading()
|
|
}
|
|
|
|
func reload() {
|
|
self.webView.reload()
|
|
}
|
|
|
|
func navigateBack() {
|
|
self.webView.goBack()
|
|
}
|
|
|
|
func navigateForward() {
|
|
self.webView.goForward()
|
|
}
|
|
|
|
func navigateTo(historyItem: BrowserContentState.HistoryItem) {
|
|
if let webItem = historyItem.webItem {
|
|
self.webView.go(to: webItem)
|
|
}
|
|
}
|
|
|
|
func scrollToTop() {
|
|
self.webView.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.webView.scrollView.contentInset.top), animated: true)
|
|
}
|
|
|
|
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ComponentTransition) {
|
|
var scrollInsets = insets
|
|
scrollInsets.top = 0.0
|
|
if self.webView.scrollView.contentInset != insets {
|
|
self.webView.scrollView.contentInset = scrollInsets
|
|
self.webView.scrollView.scrollIndicatorInsets = scrollInsets
|
|
}
|
|
self.previousScrollingOffset = ScrollingOffsetState(value: self.webView.scrollView.contentOffset.y, isDraggingOrDecelerating: self.webView.scrollView.isDragging || self.webView.scrollView.isDecelerating)
|
|
transition.setFrame(view: self.webView, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top)))
|
|
}
|
|
|
|
private func updateState(_ f: (BrowserContentState) -> BrowserContentState) {
|
|
let updated = f(self._state)
|
|
self._state = updated
|
|
self.statePromise.set(.single(self._state))
|
|
}
|
|
|
|
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
|
if keyPath == "title" {
|
|
self.updateState { $0.withUpdatedTitle(self.webView.title ?? "") }
|
|
} else if keyPath == "URL" {
|
|
self.updateState { $0.withUpdatedUrl(self.webView.url?.absoluteString ?? "") }
|
|
self.didSetupSearch = false
|
|
} else if keyPath == "estimatedProgress" {
|
|
self.updateState { $0.withUpdatedEstimatedProgress(self.webView.estimatedProgress) }
|
|
} else if keyPath == "canGoBack" {
|
|
self.updateState { $0.withUpdatedCanGoBack(self.webView.canGoBack) }
|
|
self.webView.disablesInteractiveTransitionGestureRecognizer = self.webView.canGoBack
|
|
} else if keyPath == "canGoForward" {
|
|
self.updateState { $0.withUpdatedCanGoForward(self.webView.canGoForward) }
|
|
}
|
|
}
|
|
|
|
private struct ScrollingOffsetState: Equatable {
|
|
var value: CGFloat
|
|
var isDraggingOrDecelerating: Bool
|
|
}
|
|
|
|
private var previousScrollingOffset: ScrollingOffsetState?
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
self.updateScrollingOffset(isReset: false, transition: .immediate)
|
|
}
|
|
|
|
private func snapScrollingOffsetToInsets() {
|
|
let transition = ComponentTransition(animation: .curve(duration: 0.4, curve: .spring))
|
|
self.updateScrollingOffset(isReset: false, transition: transition)
|
|
}
|
|
|
|
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
if !decelerate {
|
|
self.snapScrollingOffsetToInsets()
|
|
}
|
|
}
|
|
|
|
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
self.snapScrollingOffsetToInsets()
|
|
}
|
|
|
|
private func updateScrollingOffset(isReset: Bool, transition: ComponentTransition) {
|
|
let scrollView = self.webView.scrollView
|
|
let isInteracting = scrollView.isDragging || scrollView.isDecelerating
|
|
if let previousScrollingOffsetValue = self.previousScrollingOffset {
|
|
let currentBounds = scrollView.bounds
|
|
let offsetToTopEdge = max(0.0, currentBounds.minY - 0.0)
|
|
let offsetToBottomEdge = max(0.0, scrollView.contentSize.height - currentBounds.maxY)
|
|
|
|
let relativeOffset = scrollView.contentOffset.y - previousScrollingOffsetValue.value
|
|
self.onScrollingUpdate(ContentScrollingUpdate(
|
|
relativeOffset: relativeOffset,
|
|
absoluteOffsetToTopEdge: offsetToTopEdge,
|
|
absoluteOffsetToBottomEdge: offsetToBottomEdge,
|
|
isReset: isReset,
|
|
isInteracting: isInteracting,
|
|
transition: transition
|
|
))
|
|
}
|
|
self.previousScrollingOffset = ScrollingOffsetState(value: scrollView.contentOffset.y, isDraggingOrDecelerating: isInteracting)
|
|
|
|
var readingProgress: CGFloat = 0.0
|
|
if !scrollView.contentSize.height.isZero {
|
|
let value = (scrollView.contentOffset.y + scrollView.contentInset.top) / (scrollView.contentSize.height - scrollView.bounds.size.height + scrollView.contentInset.top)
|
|
readingProgress = max(0.0, min(1.0, value))
|
|
}
|
|
self.updateState {
|
|
$0.withUpdatedReadingProgress(readingProgress)
|
|
}
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
self.updateState {
|
|
$0
|
|
.withUpdatedBackList(webView.backForwardList.backList.map { BrowserContentState.HistoryItem(webItem: $0) })
|
|
.withUpdatedForwardList(webView.backForwardList.forwardList.map { BrowserContentState.HistoryItem(webItem: $0) })
|
|
}
|
|
|
|
self.parseFavicon()
|
|
}
|
|
|
|
@available(iOSApplicationExtension 15.0, iOS 15.0, *)
|
|
func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
|
|
decisionHandler(.prompt)
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
var completed = false
|
|
let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: message, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
|
|
if !completed {
|
|
completed = true
|
|
completionHandler()
|
|
}
|
|
})])
|
|
alertController.dismissed = { byOutsideTap in
|
|
if byOutsideTap {
|
|
if !completed {
|
|
completed = true
|
|
completionHandler()
|
|
}
|
|
}
|
|
}
|
|
self.present(alertController, nil)
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
var completed = false
|
|
let alertController = textAlertController(context: self.context, updatedPresentationData: nil, title: nil, text: message, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
|
|
if !completed {
|
|
completed = true
|
|
completionHandler(false)
|
|
}
|
|
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {
|
|
if !completed {
|
|
completed = true
|
|
completionHandler(true)
|
|
}
|
|
})])
|
|
alertController.dismissed = { byOutsideTap in
|
|
if byOutsideTap {
|
|
if !completed {
|
|
completed = true
|
|
completionHandler(false)
|
|
}
|
|
}
|
|
}
|
|
self.present(alertController, nil)
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
|
|
var completed = false
|
|
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: prompt, value: defaultText, apply: { value in
|
|
if !completed {
|
|
completed = true
|
|
if let value = value {
|
|
completionHandler(value)
|
|
} else {
|
|
completionHandler(nil)
|
|
}
|
|
}
|
|
})
|
|
promptController.dismissed = { byOutsideTap in
|
|
if byOutsideTap {
|
|
if !completed {
|
|
completed = true
|
|
completionHandler(nil)
|
|
}
|
|
}
|
|
}
|
|
self.present(promptController, nil)
|
|
}
|
|
|
|
@available(iOS 13.0, *)
|
|
func webView(_ webView: WKWebView, contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) {
|
|
guard let url = elementInfo.linkURL else {
|
|
completionHandler(nil)
|
|
return
|
|
}
|
|
//TODO:localize
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
|
|
return UIMenu(title: "", children: [
|
|
UIAction(title: "Open", image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Browser"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
|
self?.open(url: url.absoluteString, new: false)
|
|
}),
|
|
UIAction(title: "Open in New Tab", image: generateTintedImage(image: UIImage(bundleImageName: "Instant View/NewTab"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
|
self?.open(url: url.absoluteString, new: true)
|
|
}),
|
|
UIAction(title: "Add to Reading List", image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ReadingList"), color: presentationData.theme.contextMenu.primaryColor), handler: { _ in
|
|
let _ = try? SSReadingList.default()?.addItem(with: url, title: nil, previewText: nil)
|
|
}),
|
|
UIAction(title: "Copy Link", image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Copy"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
|
UIPasteboard.general.string = url.absoluteString
|
|
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
|
}),
|
|
UIAction(title: "Share", image: generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: presentationData.theme.contextMenu.primaryColor), handler: { [weak self] _ in
|
|
self?.share(url: url.absoluteString)
|
|
})
|
|
])
|
|
}
|
|
completionHandler(configuration)
|
|
}
|
|
|
|
private func open(url: String, new: Bool) {
|
|
let subject: BrowserScreen.Subject = .webPage(url: url)
|
|
if new, let navigationController = self.getNavigationController() {
|
|
self.minimize()
|
|
let controller = BrowserScreen(context: self.context, subject: subject)
|
|
navigationController.pushViewController(controller)
|
|
} else {
|
|
self.pushContent(subject)
|
|
}
|
|
}
|
|
|
|
private func share(url: String) {
|
|
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
|
let shareController = ShareController(context: self.context, subject: .url(url))
|
|
shareController.actionCompleted = { [weak self] in
|
|
self?.present(UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: presentationData.strings.Conversation_LinkCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil)
|
|
}
|
|
self.present(shareController, nil)
|
|
}
|
|
|
|
private func parseFavicon() {
|
|
struct Favicon: Equatable, Hashable {
|
|
let url: String
|
|
let dimensions: PixelDimensions?
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(self.url)
|
|
if let dimensions = self.dimensions {
|
|
hasher.combine(dimensions.width)
|
|
hasher.combine(dimensions.height)
|
|
}
|
|
}
|
|
}
|
|
|
|
let js = """
|
|
var favicons = [];
|
|
var nodeList = document.getElementsByTagName('link');
|
|
for (var i = 0; i < nodeList.length; i++)
|
|
{
|
|
if((nodeList[i].getAttribute('rel') == 'icon')||(nodeList[i].getAttribute('rel') == 'shortcut icon'))
|
|
{
|
|
const node = nodeList[i];
|
|
favicons.push({
|
|
url: node.getAttribute('href'),
|
|
sizes: node.getAttribute('sizes')
|
|
});
|
|
}
|
|
}
|
|
favicons;
|
|
"""
|
|
self.webView.evaluateJavaScript(js, completionHandler: { [weak self] jsResult, _ in
|
|
guard let self, let favicons = jsResult as? [Any] else {
|
|
return
|
|
}
|
|
var result = Set<Favicon>();
|
|
for favicon in favicons {
|
|
if let faviconDict = favicon as? [String: Any], let urlString = faviconDict["url"] as? String {
|
|
if let url = URL(string: urlString, relativeTo: self.webView.url) {
|
|
let sizesString = faviconDict["sizes"] as? String;
|
|
let sizeStrings = sizesString?.components(separatedBy: "x") ?? []
|
|
if (sizeStrings.count == 2) {
|
|
let width = Int(sizeStrings[0])
|
|
let height = Int(sizeStrings[1])
|
|
let dimensions: PixelDimensions?
|
|
if let width, let height {
|
|
dimensions = PixelDimensions(width: Int32(width), height: Int32(height))
|
|
} else {
|
|
dimensions = nil
|
|
}
|
|
result.insert(Favicon(url: url.absoluteString, dimensions: dimensions))
|
|
} else {
|
|
result.insert(Favicon(url: url.absoluteString, dimensions: nil))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if result.isEmpty, let webViewUrl = self.webView.url {
|
|
let schemeAndHostUrl = webViewUrl.deletingPathExtension()
|
|
let url = schemeAndHostUrl.appendingPathComponent("favicon.ico")
|
|
result.insert(Favicon(url: url.absoluteString, dimensions: nil))
|
|
}
|
|
|
|
var largestIcon = result.first(where: { $0.url.lowercased().contains(".svg") })
|
|
if largestIcon == nil {
|
|
largestIcon = result.first
|
|
for icon in result {
|
|
let maxSize = largestIcon?.dimensions?.width ?? 0
|
|
if let width = icon.dimensions?.width, width > maxSize {
|
|
largestIcon = icon
|
|
}
|
|
}
|
|
}
|
|
|
|
if let favicon = largestIcon {
|
|
self.faviconDisposable.set((fetchFavicon(context: self.context, url: favicon.url, size: CGSize(width: 20.0, height: 20.0))
|
|
|> deliverOnMainQueue).startStrict(next: { [weak self] favicon in
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.updateState { $0.withUpdatedFavicon(favicon) }
|
|
}))
|
|
}
|
|
})
|
|
}
|
|
}
|