Skip to content
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

Merged
merged 12 commits into from
Jun 8, 2017
Merged

Conversation

Shou
Copy link
Contributor

@Shou Shou commented May 12, 2017

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.

@@ -57,7 +57,8 @@
isFinish: state.is('success')
}">
<safe-webview
src="'https://etcher.io/success-banner/'"></safe-webview>
src="'https://etcher.io/'"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh?

Copy link
Contributor Author

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.

@@ -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>
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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)

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor

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? ;-)

Copy link
Contributor Author

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;
Copy link
Contributor

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() ?

Copy link
Contributor Author

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.

Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also function should be Function

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
Copy link
Contributor

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.

@@ -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>
Copy link
Contributor

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) => {
Copy link
Contributor

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.

@Shou Shou force-pushed the webview-refresh branch 4 times, most recently from a6dad66 to 8096137 Compare May 17, 2017 16:51
@Shou
Copy link
Contributor Author

Shou commented May 17, 2017

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
Copy link
Contributor

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

@jviotti
Copy link
Contributor

jviotti commented May 18, 2017

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;
Copy link
Contributor

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);
Copy link
Contributor

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?

@jviotti
Copy link
Contributor

jviotti commented May 18, 2017

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 lib/gui/components/safe-webview.js?

@jviotti
Copy link
Contributor

jviotti commented May 18, 2017

I don't think this is working. I'm doing the following:

  • Check the URL of the webview to https://etcher.io
  • Flash an image
  • In the webview, click a link on https://etcher.io (e.g. the changelog)
  • Wait for it to load
  • Click "Flash Another"
  • Flash the image again

I now see the webview on the changelog page, so it hasn't been reset.

@Shou
Copy link
Contributor Author

Shou commented May 22, 2017

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

@jviotti
Copy link
Contributor

jviotti commented May 23, 2017

I don't think I get it. Where do we pass --disable-http-cache switch? Also, can we disable caching just for the webview, but keep it enabled for the actual main window?

@Shou
Copy link
Contributor Author

Shou commented May 24, 2017

@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 'partition' property directly in the React attributes because it's an unknown attribute to React, and it strips unknown attributes. There's a big issue about it. This is why the src setting was moved to componentDidMount.

@@ -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';
Copy link
Contributor

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;
Copy link
Contributor

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.
Copy link
Contributor

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';
Copy link
Contributor

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

Copy link
Contributor

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?

Copy link
Contributor Author

@Shou Shou May 24, 2017

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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.

@jviotti
Copy link
Contributor

jviotti commented May 24, 2017

I'm getting this when clicking "Flash Another", after navigating to another page in the webview:

screenshot 2017-05-24 09 29 30

I still keep seeing the new page after a retry though. The webview is not getting reset.

@Shou
Copy link
Contributor Author

Shou commented May 24, 2017

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,
Copy link
Contributor

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? ;)

Copy link
Contributor Author

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

@lurch
Copy link
Contributor

lurch commented May 24, 2017

Unit-tests are failing with:

> etcher@1.0.0 jslint /etcher
> eslint lib tests scripts bin versionist.conf.js
/etcher/lib/gui/components/safe-webview/safe-webview.js
  106:7  error  Unexpected negated condition  no-negated-condition
✖ 1 problem (1 error, 0 warnings)

*/
componentWillUnmount() {
componentWillReceiveProps(nextProps) {
if (this.props.currentStateName === 'success' && nextProps.currentStateName !== 'success') {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@@ -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', {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@Shou
Copy link
Contributor Author

Shou commented May 30, 2017

Updated

}">
<safe-webview
src="'https://etcher.io/success-banner/'"></safe-webview>
src="'https://etcher.io/success-banner/'"
refresh-now="state.previousName === 'success'"></safe-webview>
Copy link
Contributor

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?

Copy link
Contributor Author

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 falsetrue happens and not truetrue. 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) => {
Copy link
Contributor

@jviotti jviotti May 30, 2017

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.

@jviotti
Copy link
Contributor

jviotti commented May 30, 2017

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:

screenshot 2017-05-30 11 03 50

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();
Copy link
Contributor

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);
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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? ;-)

@lurch
Copy link
Contributor

lurch commented May 30, 2017

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.
@Shou
Copy link
Contributor Author

Shou commented May 31, 2017

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 console.error and caught by Corvus inside Etcher, which means everything's working as intended, as banner pages are only supposed to use the console for Etcher-related debugging and errors intended to reach Corvus.

@lurch
Copy link
Contributor

lurch commented May 31, 2017

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?

@jviotti
Copy link
Contributor

jviotti commented Jun 1, 2017

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 webview) and transform it into a "Success banner timeout" Mixpanel event, since I'd like to know how often that happens.

@Shou
Copy link
Contributor Author

Shou commented Jun 6, 2017

So I've changed it from accepting all console messages to only specific JSON objects containing type and message now, what do you think?

@@ -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({
Copy link
Contributor

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 ?

Copy link
Contributor

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?

} else if (data.type === ERROR_LEVEL) {
analytics.logException(data.message);
}
});
Copy link
Contributor

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!'}"});

* });
*/
static consoleMessage(event) {
const LOG_LEVEL = 0;
Copy link
Contributor

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.

Copy link
Contributor

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"?

Copy link
Contributor

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.

const ERROR_LEVEL = 2;

_.attempt(() => {
const data = JSON.parse(event.message);
Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice thinking @jviotti 👍

// Make a persistent electron session for the webview
electron.remote.session.fromPartition(ELECTRON_SESSION, {

// Disable the cache for the session
Copy link
Contributor

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?

@Shou
Copy link
Contributor Author

Shou commented Jun 7, 2017

Here we go. Let me know what you think.

* });
*
* // or with robot
* robot.printError('Good night!');
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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?

Copy link
Contributor Author

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.

analytics.logException(data.message);
}
});
if (command === robot.COMMAND.ERROR) {
Copy link
Contributor

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 ?

const {
command,
data
} = _.attempt(robot.parseMessage, event.message);
Copy link
Contributor

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?

Copy link
Contributor

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.

* console.log({
* type: 0,
* message: 'Hello, World!'
* command: 'error',
Copy link
Contributor

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..."?

@Shou
Copy link
Contributor Author

Shou commented Jun 7, 2017

How about now?

@lurch
Copy link
Contributor

lurch commented Jun 7, 2017

Looks great :-)

@lurch lurch requested a review from jviotti June 7, 2017 14:13
@jviotti jviotti merged commit 31a2389 into master Jun 8, 2017
@jviotti jviotti deleted the webview-refresh branch June 8, 2017 15:43
@jviotti
Copy link
Contributor

jviotti commented Jun 8, 2017

Works amazingly well!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants