RedditWebView.swift 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import AppKit
  2. import SwiftUI
  3. import WebKit
  4. struct RedditWebView: NSViewRepresentable {
  5. let url: URL
  6. let reloadTrigger: Int
  7. func makeCoordinator() -> Coordinator {
  8. Coordinator()
  9. }
  10. func makeNSView(context: Context) -> RedditWebViewContainer {
  11. let container = RedditWebViewContainer(webView: RedditWebViewManager.makeWebView())
  12. container.webView.navigationDelegate = context.coordinator
  13. container.webView.uiDelegate = context.coordinator
  14. context.coordinator.container = container
  15. context.coordinator.lastReloadTrigger = reloadTrigger
  16. container.load(url: url)
  17. return container
  18. }
  19. func updateNSView(_ container: RedditWebViewContainer, context: Context) {
  20. let urlChanged = context.coordinator.lastLoadedURL != url
  21. let triggerChanged = context.coordinator.lastReloadTrigger != reloadTrigger
  22. guard urlChanged || triggerChanged else { return }
  23. context.coordinator.lastReloadTrigger = reloadTrigger
  24. context.coordinator.lastLoadedURL = url
  25. container.load(url: url)
  26. }
  27. final class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate {
  28. weak var container: RedditWebViewContainer?
  29. var lastReloadTrigger = -1
  30. var lastLoadedURL: URL?
  31. func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
  32. webView.evaluateJavaScript(RedditPostBoundaryStyle.injectionScript, completionHandler: nil)
  33. }
  34. func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
  35. container?.reloadLastURL()
  36. }
  37. func webView(
  38. _ webView: WKWebView,
  39. didFailProvisionalNavigation navigation: WKNavigation!,
  40. withError error: Error
  41. ) {
  42. let nsError = error as NSError
  43. guard nsError.domain != NSURLErrorDomain || nsError.code != NSURLErrorCancelled else { return }
  44. container?.reloadLastURL(after: 1.0)
  45. }
  46. func webView(
  47. _ webView: WKWebView,
  48. decidePolicyFor navigationAction: WKNavigationAction,
  49. decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
  50. ) {
  51. guard let url = navigationAction.request.url else {
  52. decisionHandler(.allow)
  53. return
  54. }
  55. if RedditWebAuthHelper.isOAuthProviderURL(url) {
  56. beginOAuthSignIn(url: url)
  57. decisionHandler(.cancel)
  58. return
  59. }
  60. if navigationAction.targetFrame == nil {
  61. webView.load(URLRequest(url: url))
  62. decisionHandler(.cancel)
  63. return
  64. }
  65. if navigationAction.navigationType == .linkActivated,
  66. !RedditWebAuthHelper.isRedditURL(url) {
  67. NSWorkspace.shared.open(url)
  68. decisionHandler(.cancel)
  69. return
  70. }
  71. decisionHandler(.allow)
  72. }
  73. func webView(
  74. _ webView: WKWebView,
  75. createWebViewWith configuration: WKWebViewConfiguration,
  76. for navigationAction: WKNavigationAction,
  77. windowFeatures: WKWindowFeatures
  78. ) -> WKWebView? {
  79. guard let url = navigationAction.request.url else { return nil }
  80. if RedditWebAuthHelper.isOAuthProviderURL(url) {
  81. beginOAuthSignIn(url: url)
  82. return nil
  83. }
  84. webView.load(URLRequest(url: url))
  85. return nil
  86. }
  87. private func beginOAuthSignIn(url: URL) {
  88. let presentationWindow = container?.window
  89. OAuthAuthenticationManager.shared.authenticate(
  90. url: url,
  91. presentationWindow: presentationWindow
  92. ) { [weak self] result in
  93. guard let self, let container = self.container else { return }
  94. switch result {
  95. case .success(let callbackURL):
  96. RedditOAuthPanelController.shared.completeSignIn(callbackURL: callbackURL) {
  97. container.load(url: RedditWebAuthHelper.redditHomeURL)
  98. }
  99. case .failure(let error as OAuthError) where error == .cancelled:
  100. break
  101. case .failure:
  102. break
  103. }
  104. }
  105. }
  106. }
  107. }
  108. final class RedditWebViewContainer: NSView {
  109. let webView: WKWebView
  110. private var pendingURL: URL?
  111. private var lastLoadedURL: URL?
  112. private var reloadWorkItem: DispatchWorkItem?
  113. init(webView: WKWebView) {
  114. self.webView = webView
  115. super.init(frame: .zero)
  116. wantsLayer = true
  117. addSubview(webView)
  118. webView.translatesAutoresizingMaskIntoConstraints = false
  119. NSLayoutConstraint.activate([
  120. webView.leadingAnchor.constraint(equalTo: leadingAnchor),
  121. webView.trailingAnchor.constraint(equalTo: trailingAnchor),
  122. webView.topAnchor.constraint(equalTo: topAnchor),
  123. webView.bottomAnchor.constraint(equalTo: bottomAnchor),
  124. ])
  125. }
  126. @available(*, unavailable)
  127. required init?(coder: NSCoder) {
  128. fatalError("init(coder:) has not been implemented")
  129. }
  130. func load(url: URL) {
  131. pendingURL = url
  132. lastLoadedURL = url
  133. reloadWorkItem?.cancel()
  134. tryLoadIfReady()
  135. }
  136. func reloadLastURL(after delay: TimeInterval = 0) {
  137. guard let lastLoadedURL else { return }
  138. reloadWorkItem?.cancel()
  139. let workItem = DispatchWorkItem { [weak self] in
  140. self?.load(url: lastLoadedURL)
  141. }
  142. reloadWorkItem = workItem
  143. DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
  144. }
  145. override func layout() {
  146. super.layout()
  147. tryLoadIfReady()
  148. }
  149. override func viewDidMoveToWindow() {
  150. super.viewDidMoveToWindow()
  151. tryLoadIfReady()
  152. }
  153. private func tryLoadIfReady() {
  154. guard let url = pendingURL else { return }
  155. guard bounds.width > 1, bounds.height > 1 else { return }
  156. pendingURL = nil
  157. webView.load(URLRequest(url: url))
  158. }
  159. }