1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-02-01 19:14:35 +02:00

[GH-314] Persist and reapply users settings in mac app (#331)

* [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
This commit is contained in:
Johannes Marbach 2021-04-28 00:09:26 +02:00 committed by GitHub
parent 97b446f609
commit 3fb078d612
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 132 additions and 14 deletions

1
.gitignore vendored
View File

@ -56,6 +56,7 @@ focalboard*.db
mac/resources/config.json
mac/temp
mac/dist
mac/*.xcodeproj/**/xcuserdata
linux/bin
linux/dist
linux/temp

View File

@ -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() {

View File

@ -7,7 +7,8 @@ import WebKit
class ViewController:
NSViewController,
WKUIDelegate,
WKNavigationDelegate {
WKNavigationDelegate,
WKScriptMessageHandler {
@IBOutlet var webView: WKWebView!
private var refreshWebViewOnLoad = true
@ -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.
@ -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.
@ -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() {
@ -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)")
}
}

View File

@ -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)

32
webapp/src/nativeApp.ts Normal file
View File

@ -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)
}

View File

@ -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'
}
@ -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 {}
}
}

View File

@ -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,
},