diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 74f05cc824..c87320a73c 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -17,7 +17,7 @@ public enum AttachmentButtonType: Equatable { case location case contact case poll - case app(PeerId, String, TelegramMediaFile) + case app(PeerId, String, [AttachMenuBots.Bot.IconName: TelegramMediaFile]) case standalone } diff --git a/submodules/AttachmentUI/Sources/AttachmentPanel.swift b/submodules/AttachmentUI/Sources/AttachmentPanel.swift index 0d61499530..9e8e5bdb0d 100644 --- a/submodules/AttachmentUI/Sources/AttachmentPanel.swift +++ b/submodules/AttachmentUI/Sources/AttachmentPanel.swift @@ -167,6 +167,7 @@ private final class AttachButtonComponent: CombinedComponent { let name: String let imageName: String var imageFile: TelegramMediaFile? + var animationFile: TelegramMediaFile? let component = context.component let strings = component.strings @@ -187,17 +188,22 @@ private final class AttachButtonComponent: CombinedComponent { case .poll: name = strings.Attachment_Poll imageName = "Chat/Attach Menu/Poll" - case let .app(_, appName, appIcon): + case let .app(_, appName, appIcons): name = appName imageName = "" - imageFile = appIcon + if let file = appIcons[.iOSAnimated] { + animationFile = file + } else if let file = appIcons[.iOSStatic] { + imageFile = file + } else if let file = appIcons[.default] { + imageFile = file + } case .standalone: name = "" imageName = "" imageFile = nil } - let tintColor = component.isSelected ? component.theme.rootController.tabBar.selectedIconColor : component.theme.rootController.tabBar.iconColor let iconSize = CGSize(width: 30.0, height: 30.0) @@ -205,12 +211,12 @@ private final class AttachButtonComponent: CombinedComponent { let spacing: CGFloat = 15.0 + UIScreenPixel let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((context.availableSize.width - iconSize.width) / 2.0), y: topInset), size: iconSize) - if let imageFile = imageFile, (imageFile.fileName ?? "").lowercased().hasSuffix(".tgs") { + if let animationFile = animationFile { let icon = animatedIcon.update( component: AnimatedStickerComponent( account: component.context.account, animation: AnimatedStickerComponent.Animation( - source: .file(media: imageFile), + source: .file(media: animationFile), loop: false, tintColor: tintColor ), @@ -610,9 +616,13 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate { } let type = self.buttons[i] - if case let .app(_, _, iconFile) = type { - if self.iconDisposables[iconFile.fileId] == nil { - self.iconDisposables[iconFile.fileId] = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: iconFile)).start() + if case let .app(_, _, iconFiles) = type { + for (name, file) in iconFiles { + if [.default, .iOSAnimated].contains(name) { + if self.iconDisposables[file.fileId] == nil { + self.iconDisposables[file.fileId] = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file)).start() + } + } } } let _ = buttonView.update( diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 87721b8d71..87ad3a83ef 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -3360,7 +3360,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, buttonText: buttonText, keepAliveSignal: nil) + let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, botId: peerId, botName: botName, url: url, queryId: nil, buttonText: buttonText, keepAliveSignal: nil, openUrl: { [weak self] url in + self?.openUrl(url, concealed: true) + }) strongSelf.present(controller, in: .window(.root)) // controller.getNavigationController = { [weak self] in // return self?.effectiveNavigationController @@ -3382,7 +3384,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, completion: { [weak self] in + let controller = standaloneWebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peerId, botId: peerId, botName: botName, url: result.url, queryId: result.queryId, buttonText: buttonText, keepAliveSignal: result.keepAliveSignal, openUrl: { [weak self] url in + self?.openUrl(url, concealed: true) + }, completion: { [weak self] in self?.chatDisplayNode.historyNode.scrollToEndOfHistory() }) strongSelf.present(controller, in: .window(.root)) @@ -10578,23 +10582,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G initialButton = .gallery } for bot in attachMenuBots.reversed() { - let iconFile: TelegramMediaFile? - if let file = bot.icons[.iOSAnimated] { - iconFile = file - } else if let file = bot.icons[.iOSStatic] { - iconFile = file - } else if let file = bot.icons[.default] { - iconFile = file - } else { - iconFile = nil - } - if let iconFile = iconFile { - let button: AttachmentButtonType = .app(bot.peer.id, bot.shortName, iconFile) - buttons.insert(button, at: 1) - - if initialButton == nil && bot.peer.id == botId { - initialButton = button - } + let button: AttachmentButtonType = .app(bot.peer.id, bot.shortName, bot.icons) + buttons.insert(button, at: 1) + + if initialButton == nil && bot.peer.id == botId { + initialButton = button } } return (buttons, initialButton) @@ -10895,9 +10887,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G let controller = strongSelf.configurePollCreation() completion(controller, nil) strongSelf.controllerNavigationDisposable.set(nil) - case let .app(botId, botName, botIcon): + case let .app(botId, botName, botIcons): let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId - let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id, botId: botId, botName: botName, url: nil, queryId: nil, buttonText: nil, keepAliveSignal: nil, replyToMessageId: replyMessageId, iconFile: botIcon) + let controller = WebAppController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peerId: peer.id, botId: botId, botName: botName, url: nil, queryId: nil, buttonText: nil, keepAliveSignal: nil, replyToMessageId: replyMessageId, iconFile: botIcons[.default]) + controller.openUrl = { [weak self] url in + self?.openUrl(url, concealed: true) + } controller.getNavigationController = { [weak self] in return self?.effectiveNavigationController } diff --git a/submodules/UrlHandling/Sources/UrlHandling.swift b/submodules/UrlHandling/Sources/UrlHandling.swift index 18b3b06f03..c48e574efc 100644 --- a/submodules/UrlHandling/Sources/UrlHandling.swift +++ b/submodules/UrlHandling/Sources/UrlHandling.swift @@ -606,6 +606,19 @@ public func isTelegramMeLink(_ url: String) -> Bool { return false } +public func isTelegraPhLink(_ url: String) -> Bool { + let schemes = ["http://", "https://", ""] + for basePath in baseTelegramMePaths { + for scheme in schemes { + let basePrefix = scheme + basePath + "/" + if url.lowercased().hasPrefix(basePrefix) { + return true + } + } + } + return false +} + public func parseProxyUrl(_ url: String) -> (host: String, port: Int32, username: String?, password: String?, secret: Data?)? { let schemes = ["http://", "https://", ""] for basePath in baseTelegramMePaths { diff --git a/submodules/WebUI/BUILD b/submodules/WebUI/BUILD index 87cb0fac7e..b950489bef 100644 --- a/submodules/WebUI/BUILD +++ b/submodules/WebUI/BUILD @@ -23,6 +23,7 @@ swift_library( "//submodules/PhotoResources:PhotoResources", "//submodules/ShimmerEffect:ShimmerEffect", "//submodules/LegacyComponents:LegacyComponents", + "//submodules/UrlHandling:UrlHandling", ], visibility = [ "//visibility:public", diff --git a/submodules/WebUI/Sources/WebAppController.swift b/submodules/WebUI/Sources/WebAppController.swift index 57cadde67c..62538ca73a 100644 --- a/submodules/WebUI/Sources/WebAppController.swift +++ b/submodules/WebUI/Sources/WebAppController.swift @@ -16,20 +16,7 @@ import HexColor import ShimmerEffect import PhotoResources import LegacyComponents - -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) - } -} +import UrlHandling public func generateWebAppThemeParams(_ presentationTheme: PresentationTheme) -> [String: Any] { var backgroundColor = presentationTheme.list.plainBackgroundColor.rgb @@ -108,6 +95,72 @@ private final class LoadingProgressNode: ASDisplayNode { } } +private final class MainButtonNode: HighlightTrackingButtonNode { + struct State { + let text: String? + let backgroundColor: UIColor + let textColor: UIColor + let isEnabled: Bool + let isVisible: Bool + } + private var state: State + + private let backgroundNode: ASDisplayNode + private let textNode: ImmediateTextNode + + override init(pointerStyle: PointerStyle? = nil) { + self.state = State(text: nil, backgroundColor: .clear, textColor: .clear, isEnabled: false, isVisible: false) + + self.backgroundNode = ASDisplayNode() + self.backgroundNode.allowsGroupOpacity = true + self.backgroundNode.isUserInteractionEnabled = false + + self.textNode = ImmediateTextNode() + self.textNode.textAlignment = .center + + super.init(pointerStyle: pointerStyle) + + self.addSubnode(self.backgroundNode) + self.backgroundNode.addSubnode(self.titleNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self, strongSelf.state.isEnabled { + if highlighted { + strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backgroundNode.alpha = 0.65 + } else { + strongSelf.backgroundNode.alpha = 1.0 + strongSelf.backgroundNode.layer.animateAlpha(from: 0.65, to: 1.0, duration: 0.2) + } + } + } + } + + func updateLayout(layout: ContainerViewLayout, state: State, transition: ContainedViewLayoutTransition) -> CGFloat { + self.state = state + + self.isUserInteractionEnabled = state.isVisible + self.isEnabled = state.isEnabled + transition.updateAlpha(node: self, alpha: state.isEnabled ? 1.0 : 0.4) + + let buttonHeight = 50.0 + if let text = state.text { + self.textNode.attributedText = NSAttributedString(string: text, font: Font.semibold(16.0), textColor: state.textColor) + + let textSize = self.textNode.updateLayout(layout.size) + self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - textSize.width) / 2.0), y: floorToScreenPixels((buttonHeight - textSize.height) / 2.0)), size: textSize) + + self.backgroundNode.backgroundColor = state.backgroundColor + } + + let totalButtonHeight = buttonHeight + layout.intrinsicInsets.bottom + transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: totalButtonHeight))) + transition.updateSublayerTransformOffset(layer: self.layer, offset: CGPoint(x: 0.0, y: state.isVisible ? 0.0 : totalButtonHeight)) + + return totalButtonHeight + } +} + public final class WebAppController: ViewController, AttachmentContainable { public var requestAttachmentMenuExpansion: () -> Void = { } public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in } @@ -115,15 +168,16 @@ public final class WebAppController: ViewController, AttachmentContainable { public var cancelPanGesture: () -> Void = { } public var isContainerPanning: () -> Bool = { return false } - private class Node: ViewControllerTracingNode, WKNavigationDelegate, UIScrollViewDelegate { + private class Node: ViewControllerTracingNode, WKNavigationDelegate, WKUIDelegate, UIScrollViewDelegate { private weak var controller: WebAppController? fileprivate var webView: WebAppWebView? private var placeholderIcon: UIImage? private var placeholderNode: ShimmerEffectNode? - private let loadingProgressNode: LoadingProgressNode + private let mainButtonNode: MainButtonNode + private var mainButtonState: MainButtonNode.State? private let context: AccountContext var presentationData: PresentationData @@ -140,6 +194,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.present = present self.loadingProgressNode = LoadingProgressNode(color: presentationData.theme.rootController.tabBar.selectedIconColor) + self.mainButtonNode = MainButtonNode() super.init() @@ -148,68 +203,24 @@ public final class WebAppController: ViewController, AttachmentContainable { } else { self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor } - - let configuration = WKWebViewConfiguration() - let userController = WKUserContentController() - - let js = "var TelegramWebviewProxyProto = function() {}; " + - "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " + - "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " + - "}; " + - "var TelegramWebviewProxy = new TelegramWebviewProxyProto();" - - let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: false) - userController.addUserScript(userScript) - userController.add(WeakGameScriptMessageHandler { [weak self] message in - if let strongSelf = self { - strongSelf.handleScriptMessage(message) - } - }, name: "performAction") - - let selectionString = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea){-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);" - let selectionScript: WKUserScript = WKUserScript(source: selectionString, injectionTime: .atDocumentEnd, forMainFrameOnly: true) - userController.addUserScript(selectionScript) - - configuration.userContentController = userController - - configuration.allowsInlineMediaPlayback = true - if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { - configuration.mediaTypesRequiringUserActionForPlayback = [] - } else if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - configuration.requiresUserActionForMediaPlayback = false - } else { - configuration.mediaPlaybackRequiresUserAction = false - } - - let webView = WebAppWebView(frame: CGRect(), configuration: configuration) + + let webView = WebAppWebView() webView.alpha = 0.0 - webView.isOpaque = false - webView.backgroundColor = .clear webView.navigationDelegate = self - - if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { - webView.allowsLinkPreview = false - } - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - webView.scrollView.contentInsetAdjustmentBehavior = .never - } - webView.interactiveTransitionGestureRecognizerTest = { point -> Bool in - return point.x > 30.0 - } - webView.allowsBackForwardNavigationGestures = false + webView.uiDelegate = self webView.scrollView.delegate = self - webView.scrollView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0) webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: [], context: nil) webView.tintColor = self.presentationData.theme.rootController.tabBar.iconColor + webView.handleScriptMessage = { [weak self] message in + self?.handleScriptMessage(message) + } self.webView = webView let placeholderNode = ShimmerEffectNode() self.addSubnode(placeholderNode) self.placeholderNode = placeholderNode self.addSubnode(self.loadingProgressNode) + self.addSubnode(self.mainButtonNode) if let iconFile = controller.iconFile { let _ = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: iconFile)).start() @@ -286,17 +297,6 @@ public final class WebAppController: ViewController, AttachmentContainable { return } self.view.addSubview(webView) - - if #available(iOS 11.0, *) { - let webScrollView = webView.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) - }) - } } private func updatePlaceholder() { @@ -307,11 +307,31 @@ public final class WebAppController: ViewController, AttachmentContainable { self.placeholderNode?.update(backgroundColor: self.backgroundColor ?? .clear, foregroundColor: theme.list.mediaPlaceholderColor, shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: [.image(image: image, rect: CGRect(origin: CGPoint(), size: image.size))], horizontal: true, size: image.size) } + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if let url = navigationAction.request.url?.absoluteString { + if isTelegramMeLink(url) || isTelegraPhLink(url) { + decisionHandler(.cancel) + self.controller?.openUrl(url) + } else { + decisionHandler(.allow) + } + } else { + decisionHandler(.allow) + } + } + + func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { + if navigationAction.targetFrame == nil, let url = navigationAction.request.url { + self.controller?.openUrl(url.absoluteString) + } + return nil + } + private var loadCount = 0 func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { self.loadCount += 1 } - + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { self.loadCount -= 1 @@ -327,6 +347,10 @@ public final class WebAppController: ViewController, AttachmentContainable { } } }) + + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) + } } func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -334,15 +358,17 @@ public final class WebAppController: ViewController, AttachmentContainable { self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) } - private var validLayout: (ContainerViewLayout, CGFloat)? func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { let previous = self.validLayout?.0 self.validLayout = (layout, navigationBarHeight) - if let webView = self.webView, let controller = self.controller { - let frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom - layout.additionalInsets.bottom))) - webView.updateFrame(frame: frame, panning: controller.isContainerPanning(), transition: transition) + if let webView = self.webView { + let frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom))) + let viewportFrame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom - layout.additionalInsets.bottom))) + transition.updateFrame(view: webView, frame: frame) + + webView.updateFrame(frame: viewportFrame, transition: transition) } if let placeholderNode = self.placeholderNode { @@ -366,16 +392,10 @@ public final class WebAppController: ViewController, AttachmentContainable { if let previous = previous, (previous.inputHeight ?? 0.0).isZero, let inputHeight = layout.inputHeight, inputHeight > 44.0 { self.controller?.requestAttachmentMenuExpansion() } - } - - func isContainerPanningUpdated(_ panning: Bool) { - guard let (layout, navigationBarHeight) = self.validLayout else { - return - } - if let webView = self.webView { - let frame = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: navigationBarHeight), size: CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: max(1.0, layout.size.height - navigationBarHeight - layout.intrinsicInsets.bottom - layout.additionalInsets.bottom))) - webView.updateFrame(frame: frame, panning: panning, transition: .immediate) + if let mainButtonState = self.mainButtonState { + let mainButtonHeight = self.mainButtonNode.updateLayout(layout: layout, state: mainButtonState, transition: transition) + transition.updateFrame(node: self.mainButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - mainButtonHeight - layout.additionalInsets.bottom), size: CGSize(width: layout.size.width, height: mainButtonHeight))) } } @@ -384,17 +404,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.loadingProgressNode.updateProgress(webView.estimatedProgress, animated: true) } } - - func animateIn() { - self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) - } - - func animateOut(completion: (() -> Void)? = nil) { - self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in - completion?() - }) - } - + private func handleScriptMessage(_ message: WKScriptMessage) { guard let body = message.body as? [String: Any] else { return @@ -409,6 +419,15 @@ public final class WebAppController: ViewController, AttachmentContainable { if let eventData = body["eventData"] as? String { self.handleSendData(data: eventData) } + case "web_app_setup_main_button": + if let eventData = body["eventData"] as? String { + print(eventData) + +// self.mainButtonState = MainButtonNode.State(text: <#T##String?#>, backgroundColor: <#T##UIColor#>, textColor: <#T##UIColor#>, isEnabled: <#T##Bool#>, isVisible: <#T##Bool#>) + if let (layout, navigationBarHeight) = self.validLayout { + self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .animated(duration: 0.2, curve: .easeInOut)) + } + } case "web_app_close": self.controller?.dismiss() default: @@ -437,13 +456,6 @@ public final class WebAppController: ViewController, AttachmentContainable { } } - func sendEvent(name: String, data: String) { - let script = "window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data))" - self.webView?.evaluateJavaScript(script, completionHandler: { _, _ in - - }) - } - func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData @@ -466,7 +478,7 @@ public final class WebAppController: ViewController, AttachmentContainable { } } themeParamsString.append("}}") - self.sendEvent(name: "theme_changed", data: themeParamsString) + self.webView?.sendEvent(name: "theme_changed", data: themeParamsString) } } @@ -491,8 +503,8 @@ public final class WebAppController: ViewController, AttachmentContainable { fileprivate let updatedPresentationData: (initial: PresentationData, signal: Signal)? private var presentationDataDisposable: Disposable? + public var openUrl: (String) -> Void = { _ in } public var getNavigationController: () -> NavigationController? = { return nil } - public var completion: () -> Void = {} public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String?, queryId: Int64?, buttonText: String?, keepAliveSignal: Signal?, replyToMessageId: MessageId?, iconFile: TelegramMediaFile?) { @@ -635,11 +647,7 @@ public final class WebAppController: ViewController, AttachmentContainable { self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) } - - public func isContainerPanningUpdated(_ panning: Bool) { - self.controllerNode.isContainerPanningUpdated(panning) - } - + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) @@ -668,10 +676,11 @@ private final class WebAppContextReferenceContentSource: ContextReferenceContent } } -public func standaloneWebAppController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String, queryId: Int64?, buttonText: String?, keepAliveSignal: Signal?, completion: @escaping () -> Void = {}) -> ViewController { +public func standaloneWebAppController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String, queryId: Int64?, buttonText: String?, keepAliveSignal: Signal?, openUrl: @escaping (String) -> Void, completion: @escaping () -> Void = {}) -> ViewController { let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: peerId), buttons: [.standalone], initialButton: .standalone) controller.requestController = { _, present in let webAppController = WebAppController(context: context, updatedPresentationData: updatedPresentationData, peerId: peerId, botId: botId, botName: botName, url: url, queryId: queryId, buttonText: buttonText, keepAliveSignal: keepAliveSignal, replyToMessageId: nil, iconFile: nil) + webAppController.openUrl = openUrl webAppController.completion = completion present(webAppController, nil) } diff --git a/submodules/WebUI/Sources/WebAppWebView.swift b/submodules/WebUI/Sources/WebAppWebView.swift index 0d6950582d..e1c9b0fd94 100644 --- a/submodules/WebUI/Sources/WebAppWebView.swift +++ b/submodules/WebUI/Sources/WebAppWebView.swift @@ -4,145 +4,106 @@ import Display import WebKit import SwiftSignalKit -private let findFixedPositionClasses = """ -function findFixedPositionClasses() { - var elems = document.body.getElementsByTagName("*"); - var len = elems.length - - var result = [] - var j = 0; - for (var i = 0; i < len; i++) { - if ((window.getComputedStyle(elems[i],null).getPropertyValue('position') == 'fixed') && (window.getComputedStyle(elems[i],null).getPropertyValue('bottom') == '0px')) { - result[j] = elems[i].className; - j++; - } - } - return result; -} -findFixedPositionClasses(); -""" - -private func findFixedPositionViews(webView: WKWebView, classes: [String]) -> [(String, UIView)] { - if let contentView = webView.scrollView.subviews.first { - func recursiveSearch(_ view: UIView) -> [(String, UIView)] { - var result: [(String, UIView)] = [] - - let description = view.description - if description.contains("class='") { - for className in classes { - if description.contains(className) { - result.append((className, view)) - break - } - } - } - - for subview in view.subviews { - result.append(contentsOf: recursiveSearch(subview)) - } - - return result - } +private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler { + private let f: (WKScriptMessage) -> () + + init(_ f: @escaping (WKScriptMessage) -> ()) { + self.f = f - return recursiveSearch(contentView) - } else { - return [] + super.init() + } + + func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) { + self.f(scriptMessage) } } final class WebAppWebView: WKWebView { - private var fixedPositionClasses: [String] = [] - private var currentFixedViews: [(String, UIView, UIView)] = [] + var handleScriptMessage: (WKScriptMessage) -> Void = { _ in } - private var timer: SwiftSignalKit.Timer? + init() { + let configuration = WKWebViewConfiguration() + let userController = WKUserContentController() + + let js = "var TelegramWebviewProxyProto = function() {}; " + + "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " + + "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " + + "}; " + + "var TelegramWebviewProxy = new TelegramWebviewProxyProto();" + + var handleScriptMessageImpl: ((WKScriptMessage) -> Void)? + let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: false) + userController.addUserScript(userScript) + userController.add(WeakGameScriptMessageHandler { message in + handleScriptMessageImpl?(message) + }, name: "performAction") + + let selectionString = "var css = '*{-webkit-touch-callout:none;} :not(input):not(textarea){-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);" + let selectionScript: WKUserScript = WKUserScript(source: selectionString, injectionTime: .atDocumentEnd, forMainFrameOnly: true) + userController.addUserScript(selectionScript) + + configuration.userContentController = userController + + configuration.allowsInlineMediaPlayback = true + if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { + configuration.mediaTypesRequiringUserActionForPlayback = [] + } else if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { + configuration.requiresUserActionForMediaPlayback = false + } else { + configuration.mediaPlaybackRequiresUserAction = false + } + + super.init(frame: CGRect(), configuration: configuration) + + self.isOpaque = false + self.backgroundColor = .clear + if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { + self.allowsLinkPreview = false + } + if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { + self.scrollView.contentInsetAdjustmentBehavior = .never + } + self.interactiveTransitionGestureRecognizerTest = { point -> Bool in + return point.x > 30.0 + } + self.allowsBackForwardNavigationGestures = false + + handleScriptMessageImpl = { [weak self] message in + if let strongSelf = self { + strongSelf.handleScriptMessage(message) + } + } + } - deinit { - self.timer?.invalidate() + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } override func didMoveToSuperview() { super.didMoveToSuperview() - if self.timer == nil { - let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in - guard let strongSelf = self else { + 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 } - - strongSelf.evaluateJavaScript(findFixedPositionClasses, completionHandler: { [weak self] result, _ in - if let result = result { - Queue.mainQueue().async { - self?.fixedPositionClasses = (result as? [String]) ?? [] - } - } - }) - }, queue: Queue.mainQueue()) - timer.start() - self.timer = timer - } - } - - - - func updateFrame(frame: CGRect, panning: Bool, transition: ContainedViewLayoutTransition) { - let reset = { [weak self] in - guard let strongSelf = self else { - return - } - for (_, view, snapshotView) in strongSelf.currentFixedViews { - view.isHidden = false - snapshotView.removeFromSuperview() - } - strongSelf.currentFixedViews = [] - } - - let update = { [weak self] in - guard let strongSelf = self else { - return - } - for (_, view, snapshotView) in strongSelf.currentFixedViews { - view.isHidden = true - - var snapshotFrame = view.frame - snapshotFrame.origin.y = frame.height - snapshotFrame.height - transition.updateFrame(view: snapshotView, frame: snapshotFrame) - } - } - - if panning { - let fixedPositionViews = findFixedPositionViews(webView: self, classes: self.fixedPositionClasses) - if fixedPositionViews.count != self.currentFixedViews.count { - var existing: [String: (UIView, UIView)] = [:] - for (className, originalView, snapshotView) in self.currentFixedViews { - existing[className] = (originalView, snapshotView) - } - - var updatedFixedViews: [(String, UIView, UIView)] = [] - for (className, view) in fixedPositionViews { - if let (_, existingSnapshotView) = existing[className] { - updatedFixedViews.append((className, view, existingSnapshotView)) - existing[className] = nil - } else if let snapshotView = view.snapshotView(afterScreenUpdates: false) { - updatedFixedViews.append((className, view, snapshotView)) - self.addSubview(snapshotView) - } - } - - for (_, originalAndSnapshotView) in existing { - originalAndSnapshotView.0.isHidden = false - originalAndSnapshotView.1.removeFromSuperview() - } - - self.currentFixedViews = updatedFixedViews - } - transition.updateFrame(view: self, frame: frame) - update() - } else { - update() - transition.updateFrame(view: self, frame: frame, completion: { _ in - reset() + contentView?.removeInteraction(dragInteraction) }) } + } + + func sendEvent(name: String, data: String) { + let script = "window.TelegramGameProxy.receiveEvent(\"\(name)\", \(data))" + self.evaluateJavaScript(script, completionHandler: { _, _ in + }) + } + func updateFrame(frame: CGRect, transition: ContainedViewLayoutTransition) { + self.sendEvent(name: "viewport_changed", data: "{height:\(frame.height)}") } }