-
Notifications
You must be signed in to change notification settings - Fork 2.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
feat(GUI): reset webview after navigating away #1422
Conversation
lib/gui/index.html
Outdated
@@ -57,7 +57,8 @@ | |||
isFinish: state.is('success') | |||
}"> | |||
<safe-webview | |||
src="'https://etcher.io/success-banner/'"></safe-webview> | |||
src="'https://etcher.io/'" |
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.
Huh?
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.
My mistake, that's for testing.
lib/gui/index.html
Outdated
@@ -57,7 +57,8 @@ | |||
isFinish: state.is('success') | |||
}"> | |||
<safe-webview | |||
src="'https://etcher.io/success-banner/'"></safe-webview> | |||
src="'https://etcher.io/'" | |||
on-state-change="state.onStateChange"></safe-webview> |
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.
Out of curiosity, what triggers this on-state-change
to happen?
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.
It happens whenever the $state
changes, e.g. user navigating to the 'settings'
, 'success'
, or back to the 'main'
page.
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.
Does that mean that changing from the 'flashing' page to the 'finished' page will also trigger a state-change, and thus a user-visible delay while the webview reloads?
Perhaps it only makes sense to reload the webview when leaving the 'finished' page? (because IMHO merely going back and forth to the settings page shouldn't cause a reload)
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.
Oh sorry, that's what it does. So onStateChange
is a higher-order function that passes the state changes to its argument function, and in there we check if the state change is from 'success'
before reloading, visible in the comment code block below.
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.
It looks weird that the safe-webview
is exposing this attribute. Can this all be handled internally?
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.
Ahhhh, thanks for the explanation. So I guess it's more like onLeaveSuccessPage
than onStateChange
? ;-)
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.
@jviotti It's the 'cleanest' way I've found so far, given the React/Angular mixing situation we're in. Basically, the React element isn't re-rendered when the attributes change, so we can't just pass an Angular variable and cause a re-render whenever that changes, and I suspect we won't want to do that anyway because if attribute changes caused a re-render it would refresh the webview. I'll look into if there's a better way though!
@lurch onStateChange
is what it says on the tin, but way it's used is more like onLeaveSuccessPage
because the first-class function we pass to onStateChange
checks if the state change is from 'success
', so we could call the function onStateChange
takes onLeaveSuccessPage
.
if (this.props.onStateChange) { | ||
this.props.onStateChange((event, toState, toParams, fromState) => { | ||
if (fromState.name === 'success' && this.state.shouldLoad) { | ||
this.refs.webview.src = this.props.src; |
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.
I guess it could also check that this.refs.webview.src != this.props.src
, to avoid an unnecessary reload()
?
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.
My thought here was in case the page has been updated, I assume some people leave Etcher open constantly to burn many cards, so this would make them see updated pages.
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.
@alexandrosm Can we tell from the analytics how many people leave Etcher open for an extended period of time? (my gut instinct would be not many people at all) How important is it that users always see the 'latest' banner, rather than the banner that they downloaded when they started Etcher?
lib/gui/app.js
Outdated
|
||
/** | ||
* @param {string} state - state page | ||
*/ | ||
this.is = $state.is; | ||
|
||
/** | ||
* @param {function} func - function to pass $rootScope to |
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 missing the @summary
, @public
, @example
, etc. Also function
should be Function
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.
Also
function
should beFunction
Does / should eslint pick that up?
lib/gui/app.js
Outdated
@@ -298,11 +298,18 @@ app.controller('HeaderController', function(SelectionStateModel, OSOpenExternalS | |||
|
|||
}); | |||
|
|||
app.controller('StateController', function($state) { | |||
app.controller('StateController', function($rootScope, $state) { | |||
|
|||
/** | |||
* @param {string} state - state page |
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.
Looks like I failed to catch it last time, but this should also include all the JSDoc tags mentioned below.
lib/gui/index.html
Outdated
@@ -57,7 +57,8 @@ | |||
isFinish: state.is('success') | |||
}"> | |||
<safe-webview | |||
src="'https://etcher.io/success-banner/'"></safe-webview> | |||
src="'https://etcher.io/'" | |||
on-state-change="state.onStateChange"></safe-webview> |
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.
It looks weird that the safe-webview
is exposing this attribute. Can this all be handled internally?
lib/gui/app.js
Outdated
/** | ||
* @param {function} func - function to pass $rootScope to | ||
*/ | ||
this.onStateChange = (func) => { |
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 this function is called multiple times, then multiple event listeners will be registered for this event.
a6dad66
to
8096137
Compare
What do you think of the way this is handled now? |
*/ | ||
componentDidMount() { | ||
|
||
// There is no element to add events to if 'shouldLoad' is false. | ||
if (this.state.shouldLoad) { | ||
// Events React is unaware of have to be handled manually |
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.
minor nit, but could you change this to "Events that React is ...", otherwise "Events React" might sound like another framework ;)
Sorry for the huge delay on this. I'll do a review tomorrow :) |
// We set the 'etcher-version' GET parameter here. | ||
url.searchParams.set('etcher-version', packageJSON.version); | ||
|
||
return url; |
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.
Can we do url.href
here, given that its the only property we use?
const url = new URL(this.props.src); | ||
|
||
// We set the 'etcher-version' GET parameter here. | ||
url.searchParams.set('etcher-version', packageJSON.version); |
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.
Can we extract etcher-version
to a constant at the top of this component?
Minor comment (and doesn't need to be addressed here), but since the safe webview element is just a single file, can we move it to |
I don't think this is working. I'm doing the following:
I now see the webview on the changelog page, so it hasn't been reset. |
@jviotti I looked into it and it has to do with the reloading being too fast and rolling back the URL change. However, I went further, and it seems like the caching is a bit aggressive here and the reloading has no effect. I believe we can disable the caching as you can see with the closing PR here. |
I don't think I get it. Where do we pass |
@jviotti Here's a working implementation. The webview now uses a specific session where the cache is disabled, so yes that's possible and done. Unfortunately we can't set the |
@@ -28,10 +28,15 @@ const packageJSON = require('../../../../package.json'); | |||
const MODULE_NAME = 'Etcher.Components.SafeWebview'; | |||
const angularSafeWebview = angular.module(MODULE_NAME, []); | |||
|
|||
const VERSION_PARAM = 'etcher-version'; |
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.
Can we document this constant?
this.refs.webview.removeEventListener('did-get-response-details', this.didGetResponseDetails); | ||
this.refs.webview.removeEventListener('new-window', this.constructor.newWindow); | ||
this.refs.webview.removeEventListener('console-message', this.constructor.consoleMessage); | ||
const DECISECOND = 100; |
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.
Can we describe what this variable is for instead? e.g. WEBVIEW_RELOAD_TIMEOUT_MS
or something like that?
makeURL() { | ||
const url = new URL(this.props.src); | ||
|
||
// We set the 'etcher-version' GET parameter here. |
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.
Can we update this comment to not hard-code the string etcher-version
?
this.refs.webview.addEventListener('new-window', this.constructor.newWindow); | ||
this.refs.webview.addEventListener('console-message', this.constructor.consoleMessage); | ||
// Use the 'success-banner' session | ||
this.refs.webview.partition = 'persist:success-banner'; |
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.
Looks like we're duplicating this same string here and in the session definition, which means it might get out of sync. Can we declare it only in one place (e.g. with a constant)?
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.
Actually, should we define the electron session in this file instead?
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.
electron.session
is only available in the main process so I don't think it's possible unfortunately.
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.
Also what's a good location to share this constant? lib/shared/constants.js
maybe?
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.
You can do electron.remote.session
from the renderer process.
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.
Let's move 'persist:success-banner'
to a constant, so we don't type it twice here, and when declaring the session.
Strange, I'm not getting any errors, but I can confirm that the resetting bug was back though. This commit should fix it. |
@@ -95,15 +101,14 @@ class SafeWebview extends react.PureComponent { | |||
componentWillReceiveProps(nextProps) { | |||
if (this.props.currentStateName === 'success' && nextProps.currentStateName !== 'success') { | |||
|
|||
// Reset URL to initial | |||
this.refs.webview.src = this.makeURL(); | |||
// Only reset the URL to initial if it has changed, otherwise reload the page, |
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.
Hahaha, isn't that what I originally suggested? ;)
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, if only I had listened lol
Unit-tests are failing with:
|
*/ | ||
componentWillUnmount() { | ||
componentWillReceiveProps(nextProps) { | ||
if (this.props.currentStateName === 'success' && nextProps.currentStateName !== 'success') { |
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 component is hard-coding a state name that is set outside of the component (in the Angular.js app), which doesn't look right. It looks like we should make the component take a boolean attribute to determine when it should be refreshed, and move the fact that in our use case that depends on the success
state outside of this component.
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 true, it should be as agnostic as possible to the rest of the program.
lib/gui/etcher.js
Outdated
@@ -39,6 +39,13 @@ electron.app.on('ready', () => { | |||
// No menu bar | |||
electron.Menu.setApplicationMenu(null); | |||
|
|||
// Make a persistent electron session called 'success-banner' | |||
electron.session.fromPartition('persist:success-banner', { |
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.
I thing I said this before, but can we move this to the safe webview component by using electron.remote.session
?
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 forgot about that, we should be able to use that.
Updated |
}"> | ||
<safe-webview | ||
src="'https://etcher.io/success-banner/'"></safe-webview> | ||
src="'https://etcher.io/success-banner/'" | ||
refresh-now="state.previousName === 'success'"></safe-webview> |
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.
I'm going to test this later, but what happens if this always evaluates to true
? Will the webview keep refreshing?
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.
I made sure to check if the previous prop was false
and the current one true
, so that a refresh is only triggered when the change from false
→ true
happens and not true
→ true
. React will sometimes trigger componentWillReceiveProps()
when it's not just a prop update, so that's also why I took this precaution.
lib/gui/app.js
Outdated
app.controller('StateController', function($state) { | ||
app.controller('StateController', function($rootScope) { | ||
|
||
$rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => { |
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 probably pedantic, but a standard practice is to register a handler for the $destroy
event of the controller and un-register any registered event.
The webview now gets successfully reset, and the uncaught error I was getting is also gone. Great work @Shou ! The only thing is that timeout errors are still printed inline in a scary way: Can we catch these error and send a nice "Success banner timeout error" Mixpanel event instead? |
this.refs.webview.removeEventListener('new-window', this.constructor.newWindow); | ||
this.refs.webview.removeEventListener('console-message', this.constructor.consoleMessage); | ||
} else { | ||
this.refs.webview.src = this.makeURL(); |
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.
Should we assign the result of this.makeURL()
to a const variable, to avoid calling the function multiple times?
this.refs.webview.removeEventListener('did-fail-load', this.didFailLoad); | ||
this.refs.webview.removeEventListener('did-get-response-details', this.didGetResponseDetails); | ||
this.refs.webview.removeEventListener('new-window', this.constructor.newWindow); | ||
this.refs.webview.removeEventListener('console-message', this.constructor.consoleMessage); |
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.
Any (easy) way to avoid the "repetition" between the addEventListener and removeEventListener calls?
* @function | ||
* @public | ||
* | ||
* @returns {URL} url object |
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.
Is this @returns
docstring now incorrect?
*/ | ||
componentDidMount() { | ||
|
||
// There is no element to add events to if 'shouldLoad' is false. | ||
if (this.state.shouldLoad) { | ||
// Events React is unaware of have to be handled manually |
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.
Could you add the that
in here too? ;-)
I think this PR also needs to be rebased on master to prevent the Ubuntu 12.04 Linux x86 builds failing issue... |
We reload and reset the webview to its original URL when the user navigates away from the success screen. Changelog-Entry: Reset webview after navigating away from success screen.
and moving the session creation to SafeWebview
I've followed the advice above now. @jviotti I'm still a bit bewildered by that error since I've never gotten it myself, but I suspect it's an error that can happen on the Etcher homepage, which is forwarded over |
Yeah, I also wonder if the error might be coming from the Etcher homepage itself, as it's very javascript-heavy. @Shou is it possible to redirect any JS errors created by the pages displayed inside the webview into a blackhole, so that they don't end up spamming the DevTools console? |
I agree with @lurch. Let's prevent any error from the banner from spamming DevTools, however please catch this particular timeout error from the Etcher side (from the |
So I've changed it from accepting all console messages to only specific JSON objects containing |
lib/gui/components/safe-webview.js
Outdated
@@ -196,16 +196,28 @@ class SafeWebview extends react.PureComponent { | |||
/** | |||
* @summary Forward console messages from the webview to analytics module | |||
* @param {Event} event - event object | |||
* | |||
* @example | |||
* console.log({ |
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.
Should this example be using consoleMessage
rather than console.log
?
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.
Ahhh, so it'd actually be in the banner page that we do the console.log
shown here, and consoleMessage
then forwards that to our analytics services? Now I understand :-)
(so perhaps the example could clarify that?)
Also, would it be worth the analytics-logging clarifying (perhaps by adding an extra property?) that these are messages being generated by whatever JS is running in the page being displayed in the webview, and not messages being generated by the core Etcher application itself?
lib/gui/components/safe-webview.js
Outdated
} else if (data.type === ERROR_LEVEL) { | ||
analytics.logException(data.message); | ||
} | ||
}); |
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.
So, just to check I'm understanding this diff correctly:
consoleMessage
takes an event
object, which is expected to have a (possibly JSON-encoded) .message
attribute? (and also previously a .level
attribute, which is now ignored)
And then if the JSON message is decoded correctly (i.e. into data
), it's then expected to have both .type
and .message
attributes?
Which I guess means the expected usage is something like:
consoleMessage({level:0, message: "{type: 0, message: 'Hello, World!'}"});
lib/gui/components/safe-webview.js
Outdated
* }); | ||
*/ | ||
static consoleMessage(event) { | ||
const LOG_LEVEL = 0; |
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.
Can we re-use our robot existing commands? See https://github.com/resin-io/etcher/blob/master/lib/shared/robot/index.js#L27. We only have "error" and "log" at the moment, but we could also add a "warning" one.
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.
Unless there's an explicit need for 'warnings', it might suffice to just stick with "error" and "log"?
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.
True, that should be more than enough.
lib/gui/components/safe-webview.js
Outdated
const ERROR_LEVEL = 2; | ||
|
||
_.attempt(() => { | ||
const data = JSON.parse(event.message); |
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.
Let's also re-use our robot module to parse messages.
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.
Nice thinking @jviotti 👍
lib/gui/components/safe-webview.js
Outdated
// Make a persistent electron session for the webview | ||
electron.remote.session.fromPartition(ELECTRON_SESSION, { | ||
|
||
// Disable the cache for the session |
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.
Can we extend this message to describe why we want to disable the cache?
Here we go. Let me know what you think. |
lib/gui/components/safe-webview.js
Outdated
* }); | ||
* | ||
* // or with robot | ||
* robot.printError('Good night!'); |
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.
But the robot
isn't exposed to the webview, is it?
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.
Nope, but maybe we could/should?
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.
I don't think we should (for security reasons, it makes sense to keep the webview as isolated as possible). And therefore this robot.printError
example probably isn't useful?
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.
Oh, I don't mean through injection, more like decoupling robot
from Etcher into its own module, but I guess if/until then I'll just remove this example.
lib/gui/components/safe-webview.js
Outdated
analytics.logException(data.message); | ||
} | ||
}); | ||
if (command === robot.COMMAND.ERROR) { |
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 should be using else if
?
lib/gui/components/safe-webview.js
Outdated
const { | ||
command, | ||
data | ||
} = _.attempt(robot.parseMessage, event.message); |
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.
It's up to @jviotti which style he prefers, but why 'unpack' the object here, instead of just checking e.g. parsedMessage.command
instead of command
later on?
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.
Although according to https://github.com/resin-io/etcher/blob/master/lib/shared/robot/README.md you're 'supposed' to use robot.getCommand(message)
and robot.getData(message)
;-)
And it looks like you can use robot.isMessage
to avoid using _.attempt
.
lib/gui/components/safe-webview.js
Outdated
* console.log({ | ||
* type: 0, | ||
* message: 'Hello, World!' | ||
* command: 'error', |
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.
Given that the Robot constants aren't available to the webview, perhaps the examples could show both 'error' and 'log' being used?
And maybe the @summary
should be "Forward specially-formatted console messages..."?
How about now? |
Looks great :-) |
Works amazingly well! |
We reload and reset the webview to its original URL when the user
navigates away from the success screen.
Changelog-Entry: Reset webview after navigating away from success
screen.