-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[GH-314] Persist and reapply users settings in mac app #331
[GH-314] Persist and reapply users settings in mac app #331
Conversation
acef353
to
6859cd2
Compare
let windowController = mainStoryBoard.instantiateController(withIdentifier: "WindowController") as! NSWindowController | ||
windowController.showWindow(self) | ||
windowController.contentViewController = tabViewController |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting the content controller isn't necessary here because it is already configured in the storyboard and will be correctly installed when loading WindowController
. I had to remove it because setting the new content controller caused the one that was already installed to disappear which then in turn triggered it to export the user settings via the code in viewWillDisappear
.
mac/Focalboard/ViewController.swift
Outdated
} | ||
} | ||
""", | ||
injectionTime: .atDocumentEnd, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @Johennes for this PR! I smoke tested it and it appears to work seamlessly (didn't notice a refresh). Some quick thoughts on how we might avoid this reload:
- What if the Swift code injected the base-64 settings string in a placeholder property (e.g. NativeApp.settingsBlob), atDocumentStart.
- Then add code to the initialization routine (somewhere in app.tsx maybe, as long as it's before rendering)
- It would check to see if Native.settingsBlob is set, if so, it would call
importUserSettings
, then delete the blob
Have to think a bit about security (I don't think that's an issue), but the extra benefit is the handling of the blob would be all in shared TS code.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a great idea! I've been able to drastically reduce the amount of JS code in the native app this way and there's no more page reload necessary. 🎉
Security-wise, I'm not an expert but I believe this at least doesn't make things worse. The injected blob is accessible by any other script running on the page but it's essentially the same data that's already accessible globally via the exportUserSettingsBlob
function.
export {UserSettings} | ||
const keys = ['language', 'theme', 'lastBoardId', 'lastViewId', 'emoji-mart.last', 'emoji-mart.frequently'] | ||
|
||
export function exportUserSettingsBlob(): string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a bit of an esoteric question but now that #310 has landed, this is mixing static class methods and standalone methods. Is there any preference for one or the other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some comments on this. but looking good :)
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)") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not really sure what does this function does other than receiving a message and printing it. Am I missign something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, that's precisely it. 😅
I had mentioned this above in the PR description:
The WebKit messaging is only needed for logging. Unfortunately, it bloats the code a little so we could also remove it maybe. Here's an example of log messages on start-up / shutdown:
I found this helpful during development but it does add some complexity so we can take it out if there are concerns. Alternatively, we could also wrap it in something like #if DEBUG ... #endif
so that we still have it available for future development but it doesn't land in the release build.
let appDelegate = NSApplication.shared.delegate as! AppDelegate | ||
let script = WKUserScript( | ||
let sessionTokenScript = WKUserScript( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we are populating localstorage from the user preferences, should we first clear it so there are no old elements getting in the way (like an upgrade/downgrade?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, good question. I wonder if that's not a general problem that the web app needs to handle? When you use Focalboard in the browser and switch to a new version, it could happen as well that localStorage
contains data from previous versions. If the semantics of the user settings change between versions, some migration logic will be needed but I'm not sure if we need to do anything special for this from the native app side. If we insert the last known settings into localStorage
on launch, it's essentially the same problem as on the web version from that point on.
const lastTimestamp = localStorage.getItem('timestamp') | ||
if (!timestamp || (lastTimestamp && Number(timestamp) <= Number(lastTimestamp))) { | ||
return false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you save whenever you change one of the settings and clear the localstorage before loading any data, you shouldn't need to check for timestamps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think that would work. The need for the timestamp stems from the fact that this PR only saves the user settings on window close and so I wanted to ensure that the settings are not imported more than once per web view. If we later switch to export the settings dynamically as they change, we can drop the time stamp. We just have to remember to also reinstall the user script when the settings change.
Summary
Since the native apps choose a random port on each launch, data stored into local storage becomes inaccessible after restarting. This PR ensures persistence of common user settings across restarts of the native mac app.
The program flow is as follows:
Exporting the settings
Importing the settings
A few notes on the implementation:
applicationWillTerminate
turned out to be insufficient because macOS allows closing all windows of an app without terminating it. As a result, whenapplicationWillTerminate
is called, there may be no windows / web views left to perform the export. As a workaround, I settled for doing the export whenever a window is closed.Ticket Link
#314