Skip to content

Commit

Permalink
[GH-314] Persist user settings in native Linux app
Browse files Browse the repository at this point in the history
This commit adds persistence for the user settings in the native Linux
app. The settings are written to $XDG_CONFIG_HOME/focalboard/settings.
If XDG_CONFIG_HOME is unset, the app falls back to $HOME/.config.

The change hooks into the existing settings export already used in the
nativve macOS app which means the settings are persisted immediately on
change.

Unfortunately, the golang webview package uses a custom native binding
technology and doesn't allow to define WebKit message handlers. As a
result, a dedicated message handler function was added to the existing
NativeApp object. In the native macOS app, said handler is
short-circuited to window.webkit.messagehandlers.[NAME].postMessage.
This has the benefit that the web app remains agnostic of the particular
native binding mechanism.

Relates to: #314
  • Loading branch information
Johennes committed Oct 9, 2021
1 parent 9ca5cd6 commit a2fa259
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 8 deletions.
93 changes: 91 additions & 2 deletions linux/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package main

import (
"encoding/base64"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"runtime"

Expand All @@ -16,6 +19,7 @@ import (
)

var sessionToken string = "su-" + uuid.New().String()
var settingsDir, settingsFile string = getSettingsPaths()

func getFreePort() (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
Expand Down Expand Up @@ -101,6 +105,86 @@ func openBrowser(url string) {
}
}

func getSettingsPaths() (dir string, file string) {
dir = fmt.Sprintf("%s/focalboard", getSettingsRoot())
file = fmt.Sprintf("%s/settings", dir)
log.Printf("Using settings file %v", file)
return
}

func getSettingsRoot() string {
xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
if len(xdgConfigHome) != 0 {
return xdgConfigHome
}

log.Println("XDG_CONFIG_HOME is not set, falling back to $HOME/.config")

home := os.Getenv("HOME")
if len(home) != 0 {
return fmt.Sprintf("%s/.config", home)
}

log.Fatal("HOME is not set, cannot store settings")
return ""
}

func loadSettings() string {
bytes, err := ioutil.ReadFile(settingsFile)
if err != nil {
log.Printf("Could not load user settings: %v", err)
return ""
}
return string(bytes)
}

func saveSettings(blob string) (err error) {
if _, statErr := os.Stat(settingsDir); os.IsNotExist(statErr) {
err = os.MkdirAll(settingsDir, 0700)
}
if err == nil {
err = ioutil.WriteFile(settingsFile, []byte(blob), 0600)
}
return
}

func receiveMessage(msg map[string]interface{}) {
msgType, ok := msg["type"].(string)
if !ok {
log.Printf("Received unexpected script message, no value for key 'type': %v ", msg)
return
}

blob, ok := msg["settingsBlob"].(string)
if !ok {
log.Println("Received unexpected script message, no value for key 'settingsBlob': %v", msg)
return
}

log.Printf("Received message %v", msgType)

switch msgType {
case "didImportUserSettings":
log.Printf("Imported user settings keys %v", msg["keys"])
case "didNotImportUserSettings":
break
case "didChangeUserSettings":
err := saveSettings(blob)
if err == nil {
log.Printf("Persisted user settings after change for key %v", msg["key"])
} else {
log.Printf("Could not persist user settings: %v", err)
}
default:
log.Printf("Received script message of unknown type %v", msgType)
}

data, err := base64.StdEncoding.DecodeString(blob)
if err == nil {
log.Printf("Current user settings: %v", string(data))
}
}

func main() {
debug := true
w := webview.New(debug)
Expand All @@ -119,8 +203,13 @@ func main() {
w.SetTitle("Focalboard")
w.SetSize(1024, 768, webview.HintNone)

script := fmt.Sprintf("localStorage.setItem('focalboardSessionId', '%s');", sessionToken)
w.Init(script)
sessionTokenScript := fmt.Sprintf("localStorage.setItem('focalboardSessionId', '%s');", sessionToken)
w.Init(sessionTokenScript)

w.Bind("receiveMessage", receiveMessage)

userSettingsScript := fmt.Sprintf("const NativeApp = { settingsBlob: \"%s\", receiveMessage: receiveMessage };", loadSettings())
w.Init(userSettingsScript)

w.Navigate(fmt.Sprintf("http://localhost:%d", port))
w.Bind("openInNewBrowser", openBrowser)
Expand Down
11 changes: 9 additions & 2 deletions mac/Focalboard/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import Cocoa
import WebKit

private let messageHandlerName = "nativeApp"

class ViewController:
NSViewController,
WKUIDelegate,
Expand All @@ -21,7 +23,7 @@ class ViewController:
webView.navigationDelegate = self
webView.uiDelegate = self
webView.isHidden = true
webView.configuration.userContentController.add(self, name: "nativeApp")
webView.configuration.userContentController.add(self, name: messageHandlerName)

clearWebViewCache()

Expand Down Expand Up @@ -81,7 +83,12 @@ class ViewController:
)
let blob = UserDefaults.standard.string(forKey: "localStorage") ?? ""
let userSettingsScript = WKUserScript(
source: "const NativeApp = { settingsBlob: \"\(blob)\" };",
source: """
const NativeApp = {
settingsBlob: \"\(blob)\",
receiveMessage: msg => window.webkit.messageHandlers.\(messageHandlerName).postMessage(msg)
};
""",
injectionTime: .atDocumentStart,
forMainFrameOnly: true
)
Expand Down
12 changes: 8 additions & 4 deletions webapp/src/nativeApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {exportUserSettingsBlob, importUserSettingsBlob} from './userSettings'

declare interface INativeApp {
settingsBlob: string | null;
receiveMessage: (msg: any) => void | null
}

declare const NativeApp: INativeApp
Expand All @@ -15,14 +16,17 @@ export function importNativeAppSettings(): void {
}
const importedKeys = importUserSettingsBlob(NativeApp.settingsBlob)
const messageType = importedKeys.length ? 'didImportUserSettings' : 'didNotImportUserSettings'
postWebKitMessage({type: messageType, settingsBlob: exportUserSettingsBlob(), keys: importedKeys})
postMessage({type: messageType, settingsBlob: exportUserSettingsBlob(), keys: importedKeys})
NativeApp.settingsBlob = null
}

export function notifySettingsChanged(key: string): void {
postWebKitMessage({type: 'didChangeUserSettings', settingsBlob: exportUserSettingsBlob(), key})
postMessage({type: 'didChangeUserSettings', settingsBlob: exportUserSettingsBlob(), key})
}

function postWebKitMessage(message: any) {
(window as any).webkit?.messageHandlers.nativeApp?.postMessage(message)
function postMessage(message: any) {
if (typeof NativeApp === 'undefined' || !NativeApp.receiveMessage) {
return
}
NativeApp.receiveMessage(message)
}

0 comments on commit a2fa259

Please sign in to comment.