Bluesky app fork with some witchin' additions 馃挮
at main 4.5 kB view raw
1import UIKit 2import WebKit 3import StoreKit 4 5class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDelegate { 6 let defaults = UserDefaults(suiteName: "group.app.witchsky") 7 8 var window: UIWindow 9 var webView: WKWebView? 10 11 var prevUrl: URL? 12 var starterPackUrl: URL? 13 14 init(window: UIWindow) { 15 self.window = window 16 super.init(nibName: nil, bundle: nil) 17 } 18 19 required init?(coder: NSCoder) { 20 fatalError("init(coder:) has not been implemented") 21 } 22 23 override func viewDidLoad() { 24 super.viewDidLoad() 25 26 let contentController = WKUserContentController() 27 contentController.add(self, name: "onMessage") 28 let configuration = WKWebViewConfiguration() 29 configuration.userContentController = contentController 30 31 let webView = WKWebView(frame: self.view.bounds, configuration: configuration) 32 webView.translatesAutoresizingMaskIntoConstraints = false 33 webView.contentMode = .scaleToFill 34 webView.navigationDelegate = self 35 self.view.addSubview(webView) 36 self.webView = webView 37 self.webView?.load(URLRequest(url: URL(string: "https://bsky.app/?splash=true&clip=true")!)) 38 } 39 40 func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 41 guard let response = message.body as? String, 42 let data = response.data(using: .utf8), 43 let payload = try? JSONDecoder().decode(WebViewActionPayload.self, from: data) else { 44 return 45 } 46 47 switch payload.action { 48 case .present: 49 self.presentAppStoreOverlay() 50 51 if let url = self.starterPackUrl { 52 defaults?.setValue(url.absoluteString, forKey: "starterPackUri") 53 } 54 case .store: 55 guard let keyToStoreAs = payload.keyToStoreAs, let jsonToStore = payload.jsonToStore else { 56 return 57 } 58 59 self.defaults?.setValue(jsonToStore, forKey: keyToStoreAs) 60 } 61 } 62 63 func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { 64 // Detect when we land on the right URL. This is incase of a short link opening the app clip 65 guard let url = navigationAction.request.url else { 66 return .allow 67 } 68 69 // Store the previous one to compare later, but only set starterPackUrl when we find the right one 70 prevUrl = url 71 // pathComponents starts with "/" as the first component, then each path name. so... 72 // ["/", "start", "name", "rkey"] 73 if isStarterPackUrl(url) { 74 self.starterPackUrl = url 75 } 76 77 return .allow 78 } 79 80 func isStarterPackUrl(_ url: URL) -> Bool { 81 var host: String? 82 if #available(iOS 16.0, *) { 83 host = url.host() 84 } else { 85 host = url.host 86 } 87 88 switch host { 89 case "bsky.app": 90 if url.pathComponents.count == 4, 91 url.pathComponents[1] == "start" || url.pathComponents[1] == "starter-pack" { 92 return true 93 } 94 return false 95 case "go.bsky.app": 96 if url.pathComponents.count == 2 { 97 return true 98 } 99 return false 100 default: 101 return false 102 } 103 } 104 105 func handleURL(url: URL) { 106 if isStarterPackUrl(url) { 107 let urlString = "\(url.absoluteString)?clip=true" 108 if let url = URL(string: urlString) { 109 self.webView?.load(URLRequest(url: url)) 110 } 111 } else { 112 self.webView?.load(URLRequest(url: URL(string: "https://bsky.app/?splash=true&clip=true")!)) 113 } 114 } 115 116 func presentAppStoreOverlay() { 117 guard let windowScene = self.window.windowScene else { 118 return 119 } 120 121 let configuration = SKOverlay.AppClipConfiguration(position: .bottomRaised) 122 let overlay = SKOverlay(configuration: configuration) 123 124 overlay.present(in: windowScene) 125 } 126 127 func getHost(_ url: URL?) -> String? { 128 if #available(iOS 16.0, *) { 129 return url?.host() 130 } else { 131 return url?.host 132 } 133 } 134 135 func getQuery(_ url: URL?) -> String? { 136 if #available(iOS 16.0, *) { 137 return url?.query() 138 } else { 139 return url?.query 140 } 141 } 142 143 func urlMatchesPrevious(_ url: URL?) -> Bool { 144 if #available(iOS 16.0, *) { 145 return url?.query() == prevUrl?.query() && url?.host() == prevUrl?.host() && url?.query() == prevUrl?.query() 146 } else { 147 return url?.query == prevUrl?.query && url?.host == prevUrl?.host && url?.query == prevUrl?.query 148 } 149 } 150} 151 152struct WebViewActionPayload: Decodable { 153 enum Action: String, Decodable { 154 case present, store 155 } 156 157 let action: Action 158 let keyToStoreAs: String? 159 let jsonToStore: String? 160}