mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Web app improvements
This commit is contained in:
parent
577f20f65d
commit
fdfe56f50a
@ -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
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -23,6 +23,7 @@ swift_library(
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/LegacyComponents:LegacyComponents",
|
||||
"//submodules/UrlHandling:UrlHandling",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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<PresentationData, NoError>)?
|
||||
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<PresentationData, NoError>)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String?, queryId: Int64?, buttonText: String?, keepAliveSignal: Signal<Never, KeepWebViewError>?, 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<PresentationData, NoError>)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String, queryId: Int64?, buttonText: String?, keepAliveSignal: Signal<Never, KeepWebViewError>?, completion: @escaping () -> Void = {}) -> ViewController {
|
||||
public func standaloneWebAppController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peerId: PeerId, botId: PeerId, botName: String, url: String, queryId: Int64?, buttonText: String?, keepAliveSignal: Signal<Never, KeepWebViewError>?, 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)
|
||||
}
|
||||
|
@ -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)}")
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user