286 lines
9.5 KiB
Swift
286 lines
9.5 KiB
Swift
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import Cocoa
|
|
import WebKit
|
|
|
|
class ViewController:
|
|
NSViewController,
|
|
WKUIDelegate,
|
|
WKNavigationDelegate,
|
|
WKScriptMessageHandler {
|
|
@IBOutlet var webView: WKWebView!
|
|
private var didLoad = false
|
|
private var refreshWebViewOnLoad = true
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
NSLog("viewDidLoad")
|
|
|
|
webView.navigationDelegate = self
|
|
webView.uiDelegate = self
|
|
webView.isHidden = true
|
|
webView.configuration.userContentController.add(self, name: "nativeApp")
|
|
|
|
clearWebViewCache()
|
|
|
|
// Load the home page if the server was started, otherwise wait until it has
|
|
let appDelegate = NSApplication.shared.delegate as! AppDelegate
|
|
if (appDelegate.isServerStarted) {
|
|
updateSessionTokenAndUserSettings()
|
|
loadHomepage()
|
|
}
|
|
|
|
// Do any additional setup after loading the view.
|
|
NotificationCenter.default.addObserver(self, selector: #selector(onServerStarted), name: AppDelegate.serverStartedNotification, object: nil)
|
|
}
|
|
|
|
override func viewDidAppear() {
|
|
super.viewDidAppear()
|
|
self.view.window?.makeFirstResponder(self.webView)
|
|
}
|
|
|
|
override func viewWillDisappear() {
|
|
super.viewWillDisappear()
|
|
persistUserSettings()
|
|
}
|
|
|
|
override var representedObject: Any? {
|
|
didSet {
|
|
// Update the view, if already loaded.
|
|
}
|
|
}
|
|
|
|
private func clearWebViewCache() {
|
|
let websiteDataTypes = NSSet(array: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache])
|
|
let date = Date(timeIntervalSince1970: 0)
|
|
WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes as! Set<String>, modifiedSince: date, completionHandler:{ })
|
|
}
|
|
|
|
@IBAction func showDiagnosticsInfo(_ sender: NSObject) {
|
|
let appDelegate = NSApplication.shared.delegate as! AppDelegate
|
|
|
|
let alert: NSAlert = NSAlert()
|
|
alert.messageText = "Diagnostics info"
|
|
alert.informativeText = "Port: \(appDelegate.serverPort)"
|
|
alert.alertStyle = .informational
|
|
alert.addButton(withTitle: "OK")
|
|
alert.runModal()
|
|
}
|
|
|
|
@objc func onServerStarted() {
|
|
NSLog("onServerStarted")
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
self.updateSessionTokenAndUserSettings()
|
|
self.loadHomepage()
|
|
}
|
|
}
|
|
|
|
private func persistUserSettings() {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
|
|
webView.evaluateJavaScript("Focalboard.exportUserSettingsBlob();") { result, error in
|
|
defer { semaphore.signal() }
|
|
guard let blob = result as? String else {
|
|
NSLog("Failed to export user settings: \(error?.localizedDescription ?? "?")")
|
|
return
|
|
}
|
|
UserDefaults.standard.set(blob, forKey: "localStorage")
|
|
NSLog("Persisted user settings: \(Data(base64Encoded: blob).flatMap { String(data: $0, encoding: .utf8) } ?? blob)")
|
|
}
|
|
|
|
// During shutdown the system grants us about 5 seconds to clean up and store user data
|
|
let timeout = DispatchTime.now() + .seconds(3)
|
|
var result: DispatchTimeoutResult?
|
|
|
|
// Busy wait because evaluateJavaScript can only be called from *and* signals on the main thread
|
|
while (result != .success && .now() < timeout) {
|
|
result = semaphore.wait(timeout: .now())
|
|
RunLoop.current.run(mode: .default, before: Date())
|
|
}
|
|
|
|
if result == .timedOut {
|
|
NSLog("Timed out trying to persist user settings")
|
|
}
|
|
}
|
|
|
|
private func updateSessionTokenAndUserSettings() {
|
|
let appDelegate = NSApplication.shared.delegate as! AppDelegate
|
|
let sessionTokenScript = WKUserScript(
|
|
source: "localStorage.setItem('focalboardSessionId', '\(appDelegate.sessionToken)');",
|
|
injectionTime: .atDocumentStart,
|
|
forMainFrameOnly: true
|
|
)
|
|
let blob = UserDefaults.standard.string(forKey: "localStorage") ?? ""
|
|
let userSettingsScript = WKUserScript(
|
|
source: "const NativeApp = { settingsBlob: \"\(blob)\" };",
|
|
injectionTime: .atDocumentStart,
|
|
forMainFrameOnly: true
|
|
)
|
|
webView.configuration.userContentController.removeAllUserScripts()
|
|
webView.configuration.userContentController.addUserScript(sessionTokenScript)
|
|
webView.configuration.userContentController.addUserScript(userSettingsScript)
|
|
}
|
|
|
|
private func loadHomepage() {
|
|
NSLog("loadHomepage")
|
|
let appDelegate = NSApplication.shared.delegate as! AppDelegate
|
|
let port = appDelegate.serverPort
|
|
let url = URL(string: "http://localhost:\(port)/")!
|
|
let request = URLRequest(url: url)
|
|
refreshWebViewOnLoad = true
|
|
webView.load(request)
|
|
}
|
|
|
|
private func downloadJsonUrl(_ url: URL) {
|
|
NSLog("downloadJsonUrl")
|
|
let prefix = "data:text/json,"
|
|
let urlString = url.absoluteString
|
|
let encodedJson = String(urlString[urlString.index(urlString.startIndex, offsetBy: prefix.lengthOfBytes(using: .utf8))...])
|
|
guard let json = encodedJson.removingPercentEncoding else {
|
|
return
|
|
}
|
|
|
|
// Form file name
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.dateFormat = "yyyy-M-d"
|
|
let dateString = dateFormatter.string(from: Date())
|
|
let filename = "archive-\(dateString).focalboard"
|
|
|
|
// Save file
|
|
let savePanel = NSSavePanel()
|
|
savePanel.canCreateDirectories = true
|
|
savePanel.nameFieldStringValue = filename
|
|
// BUGBUG: Specifying the allowedFileTypes causes Catalina to hang / error out
|
|
//savePanel.allowedFileTypes = [".focalboard"]
|
|
savePanel.begin { (result) in
|
|
if result.rawValue == NSApplication.ModalResponse.OK.rawValue,
|
|
let fileUrl = savePanel.url {
|
|
try? json.write(to: fileUrl, atomically: true, encoding: .utf8)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func downloadCsvUrl(_ url: URL) {
|
|
NSLog("downloadCsvUrl")
|
|
let prefix = "data:text/csv;charset=utf-8,"
|
|
let urlString = url.absoluteString
|
|
let encodedContents = String(urlString[urlString.index(urlString.startIndex, offsetBy: prefix.lengthOfBytes(using: .utf8))...])
|
|
guard let contents = encodedContents.removingPercentEncoding else {
|
|
return
|
|
}
|
|
|
|
let filename = "data.csv"
|
|
|
|
// Save file
|
|
let savePanel = NSSavePanel()
|
|
savePanel.canCreateDirectories = true
|
|
savePanel.nameFieldStringValue = filename
|
|
// BUGBUG: Specifying the allowedFileTypes causes Catalina to hang / error out
|
|
//savePanel.allowedFileTypes = [".focalboard"]
|
|
savePanel.begin { (result) in
|
|
if result.rawValue == NSApplication.ModalResponse.OK.rawValue,
|
|
let fileUrl = savePanel.url {
|
|
try? contents.write(to: fileUrl, atomically: true, encoding: .utf8)
|
|
}
|
|
}
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping ([URL]?) -> Void) {
|
|
NSLog("webView runOpenPanel")
|
|
let openPanel = NSOpenPanel()
|
|
openPanel.canChooseFiles = true
|
|
openPanel.begin { (result) in
|
|
if result == NSApplication.ModalResponse.OK {
|
|
if let url = openPanel.url {
|
|
completionHandler([url])
|
|
}
|
|
} else if result == NSApplication.ModalResponse.cancel {
|
|
completionHandler(nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
|
if let url = navigationAction.request.url {
|
|
// Intercept archive downloads, and present native UI
|
|
if (url.absoluteString.hasPrefix("data:text/json,")) {
|
|
decisionHandler(.cancel)
|
|
downloadJsonUrl(url)
|
|
return
|
|
}
|
|
if (url.absoluteString.hasPrefix("data:text/csv;charset=utf-8,")) {
|
|
decisionHandler(.cancel)
|
|
downloadCsvUrl(url)
|
|
return
|
|
}
|
|
NSLog("decidePolicyFor navigationAction: \(url.absoluteString)")
|
|
}
|
|
decisionHandler(.allow)
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
NSLog("webView didFinish navigation: \(webView.url?.absoluteString ?? "")")
|
|
// Disable right-click menu
|
|
webView.evaluateJavaScript("document.body.setAttribute('oncontextmenu', 'event.preventDefault();');", completionHandler: nil)
|
|
webView.isHidden = false
|
|
didLoad = true
|
|
|
|
// HACKHACK: Fix WebView initial rendering artifacts
|
|
if (refreshWebViewOnLoad) {
|
|
refreshWebViewOnLoad = false
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
|
|
self.refreshWebView()
|
|
})
|
|
}
|
|
}
|
|
|
|
// HACKHACK: Fix WebView initial rendering artifacts
|
|
private func refreshWebView() {
|
|
let frame = self.webView.frame
|
|
var frame2 = frame
|
|
frame2.size.height += 1
|
|
self.webView.frame = frame2
|
|
self.webView.frame = frame
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
|
NSLog("webView didFailProvisionalNavigation, error: \(error.localizedDescription)")
|
|
if (!didLoad) {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
self.updateSessionTokenAndUserSettings()
|
|
self.loadHomepage()
|
|
}
|
|
}
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
|
webView.isHidden = false
|
|
}
|
|
|
|
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
|
if let frame = navigationAction.targetFrame,
|
|
frame.isMainFrame {
|
|
return nil
|
|
}
|
|
// for _blank target or non-mainFrame target, open in default browser
|
|
if let url = navigationAction.request.url {
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
@IBAction func navigateToHome(_ sender: NSObject) {
|
|
loadHomepage()
|
|
}
|
|
|
|
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
guard let body = message.body as? [String: String], let type = body["type"], let blob = body["settingsBlob"] else {
|
|
NSLog("Received unexpected script message \(message.body)")
|
|
return
|
|
}
|
|
NSLog("Received script message \(type): \(Data(base64Encoded: blob).flatMap { String(data: $0, encoding: .utf8) } ?? blob)")
|
|
}
|
|
}
|
|
|