Skip to content

Commit

Permalink
[mattermost-communityGH-314] Persist and reapply users settings in ma…
Browse files Browse the repository at this point in the history
…c app

Relates to: mattermost-community#314
  • Loading branch information
Johennes committed Apr 25, 2021
1 parent 4a5d7c4 commit 6859cd2
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 12 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ focalboard*.db
mac/resources/config.json
mac/temp
mac/dist
mac/*.xcodeproj/**/xcuserdata
linux/bin
linux/dist
linux/temp
Expand Down
2 changes: 0 additions & 2 deletions mac/Focalboard/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {

@IBAction func openNewWindow(_ sender: Any?) {
let mainStoryBoard = NSStoryboard(name: "Main", bundle: nil)
let tabViewController = mainStoryBoard.instantiateController(withIdentifier: "ViewController") as? ViewController
let windowController = mainStoryBoard.instantiateController(withIdentifier: "WindowController") as! NSWindowController
windowController.showWindow(self)
windowController.contentViewController = tabViewController
}

private func showWhatsNewDialogIfNeeded() {
Expand Down
90 changes: 83 additions & 7 deletions mac/Focalboard/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@
import Cocoa
import WebKit

private let messageHandlerName = "callback"

private enum MessageType: String {
case didImportUserSettings
case didNotImportUserSettings
}

class ViewController:
NSViewController,
WKUIDelegate,
WKNavigationDelegate {
WKNavigationDelegate,
WKScriptMessageHandler {
@IBOutlet var webView: WKWebView!
private var refreshWebViewOnLoad = true

Expand All @@ -19,14 +27,15 @@ class ViewController:
webView.navigationDelegate = self
webView.uiDelegate = self
webView.isHidden = true
webView.configuration.userContentController.add(self, name: messageHandlerName)

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) {
self.updateSessionToken()
self.loadHomepage()
updateSessionTokenAndUserSettings()
loadHomepage()
}

// Do any additional setup after loading the view.
Expand All @@ -38,6 +47,11 @@ class ViewController:
self.view.window?.makeFirstResponder(self.webView)
}

override func viewWillDisappear() {
super.viewWillDisappear()
persistUserSettings()
}

override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
Expand All @@ -64,20 +78,70 @@ class ViewController:
@objc func onServerStarted() {
NSLog("onServerStarted")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.updateSessionToken()
self.updateSessionTokenAndUserSettings()
self.loadHomepage()
}
}

private func updateSessionToken() {
private func persistUserSettings() {
let semaphore = DispatchSemaphore(value: 0)

// Convert to base64 to avoid escaping issues when later inserting the exported settings into the import script
webView.evaluateJavaScript("window.btoa(Focalboard.exportUserSettings())") { result, error in
defer { semaphore.signal() }
guard let base64 = result as? String, let data = Data(base64Encoded: base64), let decoded = String(data: data, encoding: .utf8) else {
NSLog("Failed to export user settings: \(error?.localizedDescription ?? "?")")
return
}
UserDefaults.standard.set(base64, forKey: "localStorage")
NSLog("Persisted user settings: \(decoded)")
}

// 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 script = WKUserScript(
let sessionTokenScript = WKUserScript(
source: "localStorage.setItem('focalboardSessionId', '\(appDelegate.sessionToken)');",
injectionTime: .atDocumentStart,
forMainFrameOnly: true
)
let base64 = UserDefaults.standard.string(forKey: "localStorage") ?? ""
let userSettingsScript = WKUserScript(
source: """
const settings = window.atob("\(base64)");
if (typeof(Focalboard) !== 'undefined') {
if (Focalboard.importUserSettings(settings)) {
window.webkit.messageHandlers.\(messageHandlerName).postMessage({
type: '\(MessageType.didImportUserSettings.rawValue)',
settings: settings
});
} else {
window.webkit.messageHandlers.\(messageHandlerName).postMessage({
type: '\(MessageType.didNotImportUserSettings.rawValue)'
});
}
}
""",
injectionTime: .atDocumentEnd,
forMainFrameOnly: true
)
webView.configuration.userContentController.removeAllUserScripts()
webView.configuration.userContentController.addUserScript(script)
webView.configuration.userContentController.addUserScript(sessionTokenScript)
webView.configuration.userContentController.addUserScript(userSettingsScript)
}

private func loadHomepage() {
Expand Down Expand Up @@ -219,5 +283,17 @@ class ViewController:
@IBAction func navigateToHome(_ sender: NSObject) {
loadHomepage()
}

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let body = message.body as? [String: Any], let rawType = body["type"] as? String, let type = MessageType(rawValue: rawType) else {
return
}
switch type {
case .didImportUserSettings:
NSLog("Imported user settings: \(body["settings"] ?? "?")")
case .didNotImportUserSettings:
NSLog("Skipped importing stale, empty or invalid user settings")
}
}
}

32 changes: 32 additions & 0 deletions webapp/src/userSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

const keys = ['language', 'theme', 'lastBoardId', 'lastViewId', 'emoji-mart.last', 'emoji-mart.frequently']

export function exportUserSettings(): string {
const settings = Object.fromEntries(keys.map((key) => [key, localStorage.getItem(key)]))
settings.timestamp = `${Date.now()}`
return JSON.stringify(settings)
}

export function importUserSettings(json: string): boolean {
const settings = parseUserSettings(json)
const timestamp = settings.timestamp
const lastTimestamp = localStorage.getItem('timestamp')
if (!timestamp || (lastTimestamp && Number(timestamp) <= Number(lastTimestamp))) {
return false
}
for (const [key, value] of Object.entries(settings)) {
localStorage.setItem(key, value as string)
}
location.reload()
return true
}

function parseUserSettings(json: string): any {
try {
return JSON.parse(json)
} catch (e) {
return {}
}
}
5 changes: 2 additions & 3 deletions webapp/webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,9 @@ function makeCommonConfig() {
publicPath: '{{.BaseURL}}/',
}),
],
entry: {
main: './src/main.tsx',
},
entry: ['./src/main.tsx', './src/userSettings.ts'],
output: {
library: "Focalboard",
filename: 'static/[name].js',
path: outpath,
},
Expand Down

0 comments on commit 6859cd2

Please sign in to comment.