Skip to content

Commit

Permalink
[GH-314] Persist and reapply users settings in mac app (#331)
Browse files Browse the repository at this point in the history
* [GH-314] Persist and reapply users settings in mac app

Relates to: #314

* Inject settings blob at document start, push base64 conversion into TS, use proper quotes

* Remove whitespace

* Rename base64 to blob for consistency
  • Loading branch information
Johennes committed Apr 27, 2021
1 parent 97b446f commit 3fb078d
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 14 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
64 changes: 57 additions & 7 deletions mac/Focalboard/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import WebKit
class ViewController:
NSViewController,
WKUIDelegate,
WKNavigationDelegate {
WKNavigationDelegate,
WKScriptMessageHandler {
@IBOutlet var webView: WKWebView!
private var refreshWebViewOnLoad = true

Expand All @@ -19,14 +20,15 @@ class ViewController:
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) {
self.updateSessionToken()
self.loadHomepage()
updateSessionTokenAndUserSettings()
loadHomepage()
}

// Do any additional setup after loading the view.
Expand All @@ -38,6 +40,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 +71,55 @@ 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)

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 script = WKUserScript(
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(script)
webView.configuration.userContentController.addUserScript(sessionTokenScript)
webView.configuration.userContentController.addUserScript(userSettingsScript)
}

private func loadHomepage() {
Expand Down Expand Up @@ -219,5 +261,13 @@ class ViewController:
@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)")
}
}

3 changes: 3 additions & 0 deletions webapp/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ import RegisterPage from './pages/registerPage'
import {IUser} from './user'
import {Utils} from './utils'
import CombinedProviders from './combinedProviders'
import {importNativeAppSettings} from './nativeApp'

const App = React.memo((): JSX.Element => {
importNativeAppSettings()

const [language, setLanguage] = useState(getCurrentLanguage())
const [user, setUser] = useState<IUser|undefined>(undefined)
const [initialLoad, setInitialLoad] = useState(false)
Expand Down
32 changes: 32 additions & 0 deletions webapp/src/nativeApp.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.

import {importUserSettingsBlob} from './userSettings'

declare interface INativeApp {
settingsBlob: string | null;
}

declare const NativeApp: INativeApp

export function importNativeAppSettings() {
if (typeof NativeApp === 'undefined' || !NativeApp.settingsBlob) {
return
}
const success = importUserSettingsBlob(NativeApp.settingsBlob)
const messageType = success ? 'didImportUserSettings' : 'didNotImportUserSettings'
postWebKitMessage({type: messageType, settingsBlob: NativeApp.settingsBlob})
NativeApp.settingsBlob = null
}

function postWebKitMessage(message: any) {
const webkit = (window as any).webkit
if (typeof webkit === 'undefined') {
return
}
const handler = webkit.messageHandlers.nativeApp
if (typeof handler === 'undefined') {
return
}
handler.postMessage(message)
}
39 changes: 37 additions & 2 deletions webapp/src/userSettings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

class UserSettings {
export class UserSettings {
static get prefillRandomIcons(): boolean {
return localStorage.getItem('randomIcons') !== 'false'
}
Expand All @@ -11,4 +11,39 @@ class UserSettings {
}
}

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

export function exportUserSettingsBlob(): string {
return window.btoa(exportUserSettings())
}

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

export function importUserSettingsBlob(blob: string): boolean {
return importUserSettings(window.atob(blob))
}

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)
}
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 3fb078d

Please sign in to comment.