mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
parent
d40e803719
commit
cb802c524e
23 changed files with 125 additions and 3901 deletions
|
|
@ -61,7 +61,6 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/AvatarComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertCheckComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent",
|
||||
"//submodules/WebWorkerShim",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -140,6 +140,53 @@ public func generateWebAppThemeParams(_ theme: PresentationTheme) -> [String: An
|
|||
]
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private let registeredProtocols: Void = {
|
||||
class AppURLProtocol: URLProtocol {
|
||||
var urlTask: URLSessionDataTask?
|
||||
|
||||
override class func canInit(with request: URLRequest) -> Bool {
|
||||
if request.url?.scheme == "https" {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
override func startLoading() {
|
||||
super.startLoading()
|
||||
|
||||
/*if self.urlTask != nil {
|
||||
return
|
||||
}
|
||||
self.urlTask = URLSession.shared.dataTask(with: self.request, completionHandler: { [weak self] _, response, error in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let error {
|
||||
self.client?.urlProtocol(self, didFailWithError: error)
|
||||
} else {
|
||||
if let response = response as? HTTPURLResponse {
|
||||
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
|
||||
} else {
|
||||
}
|
||||
self.client?.urlProtocolDidFinishLoading(self)
|
||||
}
|
||||
})
|
||||
self.urlTask?.resume()*/
|
||||
}
|
||||
|
||||
override func stopLoading() {
|
||||
self.urlTask?.cancel()
|
||||
}
|
||||
}
|
||||
URLProtocol.registerClass(AppURLProtocol.self)
|
||||
}()
|
||||
#endif
|
||||
|
||||
public final class WebAppController: ViewController, AttachmentContainable {
|
||||
public var requestAttachmentMenuExpansion: () -> Void = { }
|
||||
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in }
|
||||
|
|
@ -204,6 +251,10 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
init(context: AccountContext, controller: WebAppController) {
|
||||
#if DEBUG
|
||||
let _ = registeredProtocols
|
||||
#endif
|
||||
|
||||
self.context = context
|
||||
self.controller = controller
|
||||
self.presentationData = controller.presentationData
|
||||
|
|
@ -220,12 +271,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
|
||||
}
|
||||
|
||||
let webView: WebAppWebView
|
||||
if context.sharedContext.immediateExperimentalUISettings.enablePWA {
|
||||
webView = WebAppPWAWebViewImpl(account: context.account)
|
||||
} else {
|
||||
webView = WebAppWebViewImpl(account: context.account)
|
||||
}
|
||||
let webView = WebAppWebView(account: context.account)
|
||||
webView.alpha = 0.0
|
||||
webView.navigationDelegate = self
|
||||
webView.uiDelegate = self
|
||||
|
|
@ -433,12 +479,45 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
webView.scrollView.insertSubview(self.topOverscrollNode.view, at: 0)
|
||||
}
|
||||
|
||||
private func load(url: URL, isMainURL: Bool) {
|
||||
if isMainURL {
|
||||
self.webView?.loadMainUrl(url: url)
|
||||
} else {
|
||||
self.webView?.load(URLRequest(url: url))
|
||||
private func load(url: URL) {
|
||||
/*#if DEBUG
|
||||
if "".isEmpty {
|
||||
if #available(iOS 16.0, *) {
|
||||
let documentsPath = URL.documentsDirectory.path(percentEncoded: false)
|
||||
|
||||
var hasher = SHA256()
|
||||
var urlString = url.absoluteString
|
||||
if let range = urlString.firstRange(of: "#") {
|
||||
urlString.removeSubrange(range.lowerBound...)
|
||||
}
|
||||
hasher.update(data: urlString.data(using: .utf8)!)
|
||||
let digest = Data(hasher.finalize())
|
||||
let urlHash = hexString(digest)
|
||||
|
||||
let cachedFilePath = documentsPath.appending("\(urlHash).bin")
|
||||
|
||||
Task {
|
||||
do {
|
||||
let data: Data
|
||||
if let cachedData = try? Data(contentsOf: URL(fileURLWithPath: cachedFilePath)) {
|
||||
data = cachedData
|
||||
print("Loaded from cache at \(cachedFilePath)")
|
||||
} else {
|
||||
let (loadedData, _) = try await URLSession.shared.data(from: url)
|
||||
data = loadedData
|
||||
try loadedData.write(to: URL(fileURLWithPath: cachedFilePath), options: .atomic)
|
||||
}
|
||||
self.webView?.load(data, mimeType: "text/html", characterEncodingName: "utf-8", baseURL: url)
|
||||
} catch let e {
|
||||
print("\(e)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
#endif*/
|
||||
self.webView?.load(URLRequest(url: url))
|
||||
}
|
||||
|
||||
func setupWebView() {
|
||||
|
|
@ -449,7 +528,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
if let url = controller.url, controller.source != .menu {
|
||||
self.queryId = controller.queryId
|
||||
if let parsedUrl = URL(string: url) {
|
||||
self.load(url: parsedUrl, isMainURL: true)
|
||||
self.load(url: parsedUrl)
|
||||
}
|
||||
if let keepAliveSignal = controller.keepAliveSignal {
|
||||
self.keepAliveDisposable = (keepAliveSignal
|
||||
|
|
@ -472,7 +551,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
}
|
||||
if let parsedUrl = URL(string: result.url) {
|
||||
strongSelf.queryId = result.queryId
|
||||
strongSelf.load(url: parsedUrl, isMainURL: true)
|
||||
strongSelf.load(url: parsedUrl)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
|
@ -492,21 +571,17 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||
return
|
||||
}
|
||||
self.controller?.titleView?.title = WebAppTitle(title: botApp.title, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: controller.botVerified)
|
||||
self.load(url: parsedUrl, isMainURL: true)
|
||||
self.load(url: parsedUrl)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
var enableCached = false
|
||||
if self.context.sharedContext.immediateExperimentalUISettings.enablePWA {
|
||||
enableCached = true
|
||||
}
|
||||
let _ = (self.context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, payload: controller.payload, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: controller.source == .menu, replyToMessageId: controller.replyToMessageId, threadId: controller.threadId, enableCached: enableCached)
|
||||
let _ = (self.context.engine.messages.requestWebView(peerId: controller.peerId, botId: controller.botId, url: controller.url, payload: controller.payload, themeParams: generateWebAppThemeParams(presentationData.theme), fromMenu: controller.source == .menu, replyToMessageId: controller.replyToMessageId, threadId: controller.threadId)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
guard let strongSelf = self, let parsedUrl = URL(string: result.url) else {
|
||||
return
|
||||
}
|
||||
strongSelf.queryId = result.queryId
|
||||
strongSelf.load(url: parsedUrl, isMainURL: true)
|
||||
strongSelf.load(url: parsedUrl)
|
||||
|
||||
if let keepAliveSignal = result.keepAliveSignal {
|
||||
strongSelf.keepAliveDisposable = (keepAliveSignal
|
||||
|
|
|
|||
|
|
@ -1,293 +0,0 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import WebKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import WebWorkerShim
|
||||
|
||||
private let findActiveElementY = """
|
||||
function getOffset(el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left + window.scrollX,
|
||||
top: rect.top + window.scrollY
|
||||
};
|
||||
}
|
||||
getOffset(document.activeElement).top;
|
||||
"""
|
||||
|
||||
private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
private let f: (WKScriptMessage) -> ()
|
||||
|
||||
init(_ f: @escaping (WKScriptMessage) -> ()) {
|
||||
self.f = f
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) {
|
||||
self.f(scriptMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private class WebViewTouchGestureRecognizer: UITapGestureRecognizer {
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
self.state = .began
|
||||
}
|
||||
}
|
||||
|
||||
private let eventProxySource = "var TelegramWebviewProxyProto = function() {}; " +
|
||||
"TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " +
|
||||
"window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " +
|
||||
"}; " +
|
||||
"var TelegramWebviewProxy = new TelegramWebviewProxyProto();"
|
||||
|
||||
private let selectionSource = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea):not([\"contenteditable\"=\"true\"]){-webkit-user-select:none;}';"
|
||||
+ " var head = document.head || document.getElementsByTagName('head')[0];"
|
||||
+ " var style = document.createElement('style'); style.type = 'text/css';" +
|
||||
" style.appendChild(document.createTextNode(css)); head.appendChild(style);"
|
||||
|
||||
private let videoSource = """
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
function tgBrowserDisableWebkitEnterFullscreen(videoElement) {
|
||||
if (videoElement && videoElement.webkitEnterFullscreen) {
|
||||
videoElement.setAttribute('playsinline', '');
|
||||
}
|
||||
}
|
||||
|
||||
function tgBrowserDisableFullscreenOnExistingVideos() {
|
||||
document.querySelectorAll('video').forEach(tgBrowserDisableWebkitEnterFullscreen);
|
||||
}
|
||||
|
||||
function tgBrowserHandleMutations(mutations) {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
|
||||
mutation.addedNodes.forEach((newNode) => {
|
||||
if (newNode.tagName === 'VIDEO') {
|
||||
tgBrowserDisableWebkitEnterFullscreen(newNode);
|
||||
}
|
||||
if (newNode.querySelectorAll) {
|
||||
newNode.querySelectorAll('video').forEach(tgBrowserDisableWebkitEnterFullscreen);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tgBrowserDisableFullscreenOnExistingVideos();
|
||||
|
||||
const _tgbrowser_observer = new MutationObserver(tgBrowserHandleMutations);
|
||||
|
||||
_tgbrowser_observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
function tgBrowserDisconnectObserver() {
|
||||
_tgbrowser_observer.disconnect();
|
||||
}
|
||||
});
|
||||
"""
|
||||
|
||||
final class WebAppPWAWebViewImpl: PWAWebView, WebAppWebView {
|
||||
var handleScriptMessage: (WKScriptMessage) -> Void = { _ in }
|
||||
|
||||
var customInsets: UIEdgeInsets = .zero {
|
||||
didSet {
|
||||
if self.customInsets != oldValue {
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var safeAreaInsets: UIEdgeInsets {
|
||||
return UIEdgeInsets(top: self.customInsets.top, left: self.customInsets.left, bottom: self.customInsets.bottom, right: self.customInsets.right)
|
||||
}
|
||||
|
||||
init(account: Account) {
|
||||
let configuration = WKWebViewConfiguration()
|
||||
|
||||
if #available(iOS 17.0, *) {
|
||||
var uuid: UUID?
|
||||
if let current = UserDefaults.standard.object(forKey: "TelegramWebStoreUUID_\(account.id.int64)") as? String {
|
||||
uuid = UUID(uuidString: current)!
|
||||
} else {
|
||||
let mainAccountId: Int64
|
||||
if let current = UserDefaults.standard.object(forKey: "TelegramWebStoreMainAccountId") as? Int64 {
|
||||
mainAccountId = current
|
||||
} else {
|
||||
mainAccountId = account.id.int64
|
||||
UserDefaults.standard.set(mainAccountId, forKey: "TelegramWebStoreMainAccountId")
|
||||
}
|
||||
|
||||
if account.id.int64 != mainAccountId {
|
||||
uuid = UUID()
|
||||
UserDefaults.standard.set(uuid!.uuidString, forKey: "TelegramWebStoreUUID_\(account.id.int64)")
|
||||
}
|
||||
}
|
||||
|
||||
if let uuid {
|
||||
configuration.websiteDataStore = WKWebsiteDataStore(forIdentifier: uuid)
|
||||
}
|
||||
}
|
||||
|
||||
let contentController = WKUserContentController()
|
||||
|
||||
var handleScriptMessageImpl: ((WKScriptMessage) -> Void)?
|
||||
let eventProxyScript = WKUserScript(source: eventProxySource, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
||||
contentController.addUserScript(eventProxyScript)
|
||||
contentController.add(WeakGameScriptMessageHandler { message in
|
||||
handleScriptMessageImpl?(message)
|
||||
}, name: "performAction")
|
||||
|
||||
let selectionScript = WKUserScript(source: selectionSource, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
|
||||
contentController.addUserScript(selectionScript)
|
||||
|
||||
let videoScript = WKUserScript(source: videoSource, injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
||||
contentController.addUserScript(videoScript)
|
||||
|
||||
configuration.userContentController = contentController
|
||||
|
||||
configuration.allowsInlineMediaPlayback = true
|
||||
configuration.allowsPictureInPictureMediaPlayback = false
|
||||
if #available(iOS 10.0, *) {
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = []
|
||||
} else {
|
||||
configuration.mediaPlaybackRequiresUserAction = false
|
||||
}
|
||||
|
||||
super.init(frame: CGRect(), configuration: configuration)
|
||||
|
||||
self.disablesInteractiveKeyboardGestureRecognizer = true
|
||||
|
||||
self.isOpaque = false
|
||||
self.backgroundColor = .clear
|
||||
if #available(iOS 9.0, *) {
|
||||
self.allowsLinkPreview = false
|
||||
}
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
self.interactiveTransitionGestureRecognizerTest = { point -> Bool in
|
||||
return point.x > 30.0
|
||||
}
|
||||
self.allowsBackForwardNavigationGestures = false
|
||||
if #available(iOS 16.4, *) {
|
||||
self.isInspectable = true
|
||||
}
|
||||
|
||||
handleScriptMessageImpl = { [weak self] message in
|
||||
if let strongSelf = self {
|
||||
strongSelf.handleScriptMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
print()
|
||||
}
|
||||
|
||||
func loadMainUrl(url: URL) {
|
||||
var url = url
|
||||
if let range = url.absoluteString.range(of: "#") {
|
||||
url = URL(string: String(url.absoluteString[..<range.lowerBound])) ?? url
|
||||
}
|
||||
self.loadPWA(url: url)
|
||||
}
|
||||
|
||||
override func didMoveToSuperview() {
|
||||
super.didMoveToSuperview()
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
let webScrollView = self.subviews.compactMap { $0 as? UIScrollView }.first
|
||||
Queue.mainQueue().after(0.1, {
|
||||
let contentView = webScrollView?.subviews.first(where: { $0.interactions.count > 1 })
|
||||
guard let dragInteraction = (contentView?.interactions.compactMap { $0 as? UIDragInteraction }.first) else {
|
||||
return
|
||||
}
|
||||
contentView?.removeInteraction(dragInteraction)
|
||||
})
|
||||
|
||||
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func hideScrollIndicators() {
|
||||
var hiddenViews: [UIView] = []
|
||||
for view in self.scrollView.subviews.reversed() {
|
||||
let minSize = min(view.frame.width, view.frame.height)
|
||||
if minSize < 4.0 {
|
||||
view.isHidden = true
|
||||
hiddenViews.append(view)
|
||||
}
|
||||
}
|
||||
Queue.mainQueue().after(2.0) {
|
||||
for view in hiddenViews {
|
||||
view.isHidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendEvent(name: String, data: String?) {
|
||||
let script = "window.TelegramGameProxy && window.TelegramGameProxy.receiveEvent && window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data ?? "null"))"
|
||||
self.evaluateJavaScript(script, completionHandler: { _, _ in
|
||||
})
|
||||
}
|
||||
|
||||
func updateMetrics(height: CGFloat, isExpanded: Bool, isStable: Bool, transition: ContainedViewLayoutTransition) {
|
||||
let viewportData = "{height:\(height), is_expanded:\(isExpanded ? "true" : "false"), is_state_stable:\(isStable ? "true" : "false")}"
|
||||
self.sendEvent(name: "viewport_changed", data: viewportData)
|
||||
|
||||
let safeInsetsData = "{top:\(self.customInsets.top), bottom:\(self.customInsets.bottom), left:\(self.customInsets.left), right:\(self.customInsets.right)}"
|
||||
self.sendEvent(name: "safe_area_changed", data: safeInsetsData)
|
||||
}
|
||||
|
||||
var lastTouchTimestamp: Double?
|
||||
private(set) var didTouchOnce = false
|
||||
var onFirstTouch: () -> Void = {}
|
||||
|
||||
func scrollToActiveElement(layout: ContainerViewLayout, completion: @escaping (CGPoint) -> Void, transition: ContainedViewLayoutTransition) {
|
||||
self.evaluateJavaScript(findActiveElementY, completionHandler: { result, _ in
|
||||
if let result = result as? CGFloat {
|
||||
Queue.mainQueue().async {
|
||||
let convertedY = result - self.scrollView.contentOffset.y
|
||||
let viewportHeight = self.frame.height
|
||||
if convertedY < 0.0 || (convertedY + 44.0) > viewportHeight {
|
||||
let targetOffset: CGFloat
|
||||
if convertedY < 0.0 {
|
||||
targetOffset = max(0.0, result - 36.0)
|
||||
} else {
|
||||
targetOffset = max(0.0, result + 60.0 - viewportHeight)
|
||||
}
|
||||
let contentOffset = CGPoint(x: 0.0, y: targetOffset)
|
||||
completion(contentOffset)
|
||||
transition.animateView({
|
||||
self.scrollView.contentOffset = contentOffset
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
self.lastTouchTimestamp = CACurrentMediaTime()
|
||||
if result != nil && !self.didTouchOnce {
|
||||
self.didTouchOnce = true
|
||||
self.onFirstTouch()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override var inputAccessoryView: UIView? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -89,21 +89,7 @@ function tgBrowserDisconnectObserver() {
|
|||
});
|
||||
"""
|
||||
|
||||
protocol WebAppWebView: WKWebView {
|
||||
var handleScriptMessage: (WKScriptMessage) -> Void { get set }
|
||||
var customInsets: UIEdgeInsets { get set }
|
||||
var lastTouchTimestamp: Double? { get set }
|
||||
var didTouchOnce: Bool { get }
|
||||
var onFirstTouch: () -> Void { get set }
|
||||
|
||||
func sendEvent(name: String, data: String?)
|
||||
func scrollToActiveElement(layout: ContainerViewLayout, completion: @escaping (CGPoint) -> Void, transition: ContainedViewLayoutTransition)
|
||||
func updateMetrics(height: CGFloat, isExpanded: Bool, isStable: Bool, transition: ContainedViewLayoutTransition)
|
||||
func hideScrollIndicators()
|
||||
func loadMainUrl(url: URL)
|
||||
}
|
||||
|
||||
final class WebAppWebViewImpl: WKWebView, WebAppWebView {
|
||||
final class WebAppWebView: WKWebView {
|
||||
var handleScriptMessage: (WKScriptMessage) -> Void = { _ in }
|
||||
|
||||
var customInsets: UIEdgeInsets = .zero {
|
||||
|
|
@ -205,10 +191,6 @@ final class WebAppWebViewImpl: WKWebView, WebAppWebView {
|
|||
print()
|
||||
}
|
||||
|
||||
func loadMainUrl(url: URL) {
|
||||
self.load(URLRequest(url: url))
|
||||
}
|
||||
|
||||
override func didMoveToSuperview() {
|
||||
super.didMoveToSuperview()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue