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:
parent
97b446f609
commit
3fb078d612
1
.gitignore
vendored
1
.gitignore
vendored
@ -56,6 +56,7 @@ focalboard*.db
|
||||
mac/resources/config.json
|
||||
mac/temp
|
||||
mac/dist
|
||||
mac/*.xcodeproj/**/xcuserdata
|
||||
linux/bin
|
||||
linux/dist
|
||||
linux/temp
|
||||
|
@ -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() {
|
||||
|
@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
32
webapp/src/nativeApp.ts
Normal 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)
|
||||
}
|
@ -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 {}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user