-
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
Video: Redux state changes for custom video poster #11633
Conversation
@marcinbot @lamosty @gwwar Tagging you guys to review this one as it looks like you worked on the image editor. This is the video equivalent of that. Thx! |
* @param {Function} fn Function to invoke when request is complete | ||
* @returns {Promise} A promise that resolves when the request is complete | ||
*/ | ||
Undocumented.prototype.updateVideoPoster = function( videoId, data, fn ) { |
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 a new wpcom only endpoint? Did we have any plans for Jetpack support?
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'll very much encourage using the new data layer for any and all new wpcom endpoints and would be happy to walk you through it. In the meantime, check out the API layer and HTTP layer readmes.
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.
@gwwar This is a WPCOM only endpoint, as VideoPress is a WPCOM feature. Jetpack uploads its videos to WPCOM for all VideoPress requests, not the other way around.
* @param {Object} params Poster data | ||
* @return {Function} Action thunk | ||
*/ | ||
export const updateVideoEditorPoster = ( guid, params ) => { |
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 think it'd be useful to describe the params
shape in the jsdoc
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, I think returning a thunk here is fine, though if you're up for it, there's a new data-layer pattern that we can use.
The upside is that testing becomes a lot easier, since we can usually dispatch a simple action on the dev console to trigger the entire the request. Maybe something like
dispatch( { type: VIDEO_EDITOR_POSTER_UPDATE, videoId, params } );
Ping me or @dmsnell for help on that
hasScriptLoadError, | ||
poster, | ||
posterIsUpdating, | ||
posterIsUpdated, |
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.
What's the difference here between posterIsUpdating
and posterIsUpdated
?
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.
@gwwar When the poster is being updated on the server, posterIsUpdating
gets set and the UI will show a spinner.
I had to introduce a separate posterIsUpdated
because I needed to know if the poster had been successfully updated, and if so, then close out the video editor and return to the previous screen - https://github.com/Automattic/wp-calypso/pull/11648/files#diff-35b626cb9b7af72412a7ab302d8fc3f6R67
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 sure if the commas matter as much as I made them out to, but I would definitely make sure you fix how the form is posting parameters.
]; | ||
} | ||
|
||
if ( 'at_time' in data ) { |
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 would make this with an elseif
, since you can't send both (the logic will always take the at_time
first on the server side). Also, you should have an else
at the end, that if neither is present, it will cause this to not post to the API.
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.
@dbtlr the way we would normally prefer to handle this isn't with a dangling else
at the end but rather with an early abort from the function call. you will find that we have very few else
structures in Calypso.
also, in general, I would strongly encourage working together on this to use the new data layer instead of creating a new wpcom.undocumented
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.
@dmsnell Re: using the data layer. It's currently in progress. 😉
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.
@donnapep please don't hesitate to discuss the work! the data layer is still very new and we're still iterating on the developer experience, so I would value your input
@@ -16,7 +16,8 @@ describe( 'reducer', () => { | |||
'showDrafts', | |||
'lastDraft', | |||
'contactForm', | |||
'imageEditor' | |||
'imageEditor', | |||
'videoEditor', |
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 it proper to have a final comma in a JavaScript array?
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.
in Calypso it is yeah. we encourage it for the effects on diffs
@@ -13,7 +13,8 @@ export const MODAL_VIEW_STATS = { | |||
[ ModalViews.LIST ]: 'view_list', | |||
[ ModalViews.DETAIL ]: 'view_detail', | |||
[ ModalViews.GALLERY ]: 'view_gallery', | |||
[ ModalViews.IMAGE_EDITOR ]: 'view_edit' | |||
[ ModalViews.IMAGE_EDITOR ]: 'view_edit', | |||
[ ModalViews.VIDEO_EDITOR ]: 'view_edit', |
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.
comma?
VIDEO_EDITOR_SCRIPT_LOAD_ERROR, | ||
VIDEO_EDITOR_STATE_RESET, | ||
VIDEO_EDITOR_STATE_RESET_POSTER, | ||
VIDEO_EDITOR_VIDEO_HAS_LOADED, |
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.
comma?
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.
@dbtlr I like to do this for objects so that if you need to add another property later, you don't get the extra diff on the preceding line. I see this being done occasionally in Calypso, but it's not part of the official style guide.
method: 'POST', | ||
path: `/videos/${ action.videoId }/poster`, | ||
body: 'at_time' in action.params ? action.params : undefined, | ||
formData: 'file' in action.params ? [ [ 'poster', action.params.file ] ] : undefined, |
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.
@dmsnell formData
does not appear to be sent with the POST request. Before I get into some deeper debugging, does what I have here so far look OK?
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.
possibly a bug? I will look into this
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.
A fix for this can be found in PR #12135, although it still needs more testing to ensure nothing breaks. Thx!
onFailure: action, | ||
} ) ); | ||
|
||
return next( action ); |
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.
@dmsnell I don't quite get when I should use next
vs. dispatch
. And in what sort of circumstances would I want to do neither of those things?
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.
next()
is going to be used when we're passing the given action along the middleware chain. maybe we intercept post editor updates and want to make sure that it's capital_P_dangit
. we could intercept that action, filter post_content
, and then we would want to pass it along to the next link in the chain with next()
- this is like add_filter()
in WP.
on the other hand, we might prefer to trigger some side-effect or corresponding action. maybe every time we see Wordpress
we want to bump some state. in this case we would pass along the original untouched action to next()
but since we're firing off a new action altogether we want it to start at the front of the middleware stack so we'll fire off dispatch( bumpStat( … ) )
- akin to add_action
in WP.
@coderkevin just since we recently discussed actions and filters I thought I'd poke you on this ^^^
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.
@dmsnell is there ever a case where we wouldn't want to pass the action along the middleware chain? As is we'll get weird oddities like the dispatch action not being recorded by redux dev tools if we omit return next( action )
in addition to missing any other side effect middleware that was lower on the chain.
For example in console if you dispatch:
dispatch( { type: 'ACCOUNT_RECOVERY_RESET_REQUEST' } );
We'll see the other actions recorded but not the initial action request :
https://github.com/Automattic/wp-calypso/pull/11699/files#diff-2c98cce53eb8b5257f1bbca88632820cR17
cc @southp
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.
@gwwar the actions should be recorded on their first dispatch()
so they should appear even if we don't pass them along (would be a bug if this isn't the case and we would probably just need to rearrange the middleware)
times we wouldn't want to pass along the action? maybe so. we might want to cull actions from hitting the reducers or system for some reason. not sure off-hand what these might be but it seams very plausible to me.
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 think this is all well and good, but it's a pretty privileged operation here. I feel more thought should go into what should be allowed for which Calypso extensions when we start accepting more outside contributions. (Note: this is merely an observation and no action is required for this on this current PR.)
}; | ||
|
||
export default { | ||
[ VIDEO_EDITOR_POSTER_UPDATE ]: [ dispatchRequest( updateVideoEditorPoster, updateVideoPosterSuccess, updateVideoPosterError ) ], |
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.
@dmsnell It seems that I needed to add a new action to be used explicitly by the data layer. Is this true in general?
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.
@donnapep I'm not sure what your question is. the data layer latches into existing actions which feed through the system. it handle side-effects and data fetching.
the idea is that Calypso can function as best as possible even if no data gets synchronized. so maybe we fire an event VIDEO_POSTER_UPDATE
which specifies a video and a frame for the post (is that what this is about?) then Calypso can hold onto that state the same way it would if it had just loaded that data from the API.
in this case the data layer sees that action fly by and it says to itself, "self, I know that when someone changes the video poster we also need to let WordPress.com know so that it persists and propagates, I will do just that"
now, once we have fully uploaded the data and get some response we can make a judgement. if it's as simple as saying, "use frame 1337 for the poster" then if we get a positive response back from the server showing that it stored 1337 and our state already shows 1337 (because Redux state operates independently of the middleware) then we don't actually need to do a thing. no changes are required. on the other hand, if it comes back with the wrong frame number we may need to respond in one of many possible ways: retry the operation (Calypso meanwhile is unaware of the failure), report the failure to the user, or just force Calypso to update (overwriting the customer's choice).
please let me know if this is addressing your question!
thanks for your rework on this @donnapep! instead of diving into the details at this moment I'd like to raise some higher-level questions:
doing well - I'm excited to have what I think is our first data uploader in the data layer! |
We'd like to surface an error message if the poster failed to update. But if the update succeeds, then the poster URL is returned by the endpoint and the thumbnail for the video is updated.
I saw that. This may work when uploading a custom image, although not when selecting a particular frame. I'll play around with it some.
Sounds good. Cheers! |
don't we already get an event dispatched at the point the video has fully loaded? are we showing the customer some indicator that it's loading? this is an upload, right? is there a reason to prefer a binary
is this consistent with other Calypso idioms? I thought we normally assume that an operation works and then only undo if it fails. is there something fundamentally different about the poster calling for the behavior? is setting the poster frame a long operation that warrants showing some process state?
Here is where the data layer is designed to support Calypso design. If our goal is to surface an error message, then why not simply surface an error message? We have an action already for that -
…continuing on that thought, if our intention is to store the URL for a poster frame, why not use an action that mirrors that intention - |
Nope, not for VideoPress videos. This will be added by a separate PR in this commit 68a9d5f
Yes, but not as part of Calypso. VideoPress controls the UI for this.
Nope, not an upload. This is just to initially display the video in the video editor. Once the video is loaded, then we can enable the button that allows the user to select a specific frame to use as the poster (or they can also choose to upload their own image instead of choosing a frame, this is where upload comes into play). I think I've done a poor job of communicating the context for this PR. To get a better idea of the background, the master PR #11389 might explain it better.
Honestly, I'm not sure. That's why I rely on reviewers like yourself to school me. 😉 I will say that video editor is largely modelled after image editor. With image editor, if the image can't be updated then an error message is shown onscreen. For consistency, video editor does a similar thing if the poster couldn't be updated. There's nothing to really "undo" though when updating the poster.
Uploading the image can take awhile, especially for larger images. Selecting a particular frame is generally faster. I do think the upload warrants an indicator (thinking to potentially switch to a progress bar instead of a spinner here), but perhaps nothing is needed for selecting a frame.
Thanks! This is worth investigating.
I dunno. Maybe. But what if I wanted to do something else after the poster is updated in addition to assigning the poster frame URL? Now my action no longer tells the whole story and is a bit of a misnomer. I find this naming convention a bit less future-proof. |
If we need to do more we can dispatch more actions. The point is to keep our actions specific to the intentions they convey. Ideally, if we have multiples visual changes that need to happen in response to the event then we will find some aptly-named action (such as |
* | ||
* @param {Object} state Current state | ||
* @param {Object} action Action object | ||
* @return {Object} Updated state |
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.
Boolean
} | ||
|
||
return state; | ||
}; |
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.
in this reducer, it looks like we only have a single true
value - when the action type is VIDEO_EDITOR_SHOW_ERROR
which could be much more succinctly captured in the following…
export const hasPosterUpdateError = ( state = false, { type } ) => type === VIDEO_EDITOR_SHOW_ERROR;
further, what's the point of storing a boolean status value for hasError
? do we not store what kind of error it is? what value is there in this on/off state?
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.
In this case, I don't think we necessarily care what the error is because it's not being used. We only care that the poster update wasn't successful, so that we can in turn communicate that to the user. I suppose we could log the specific error, but other than that it wouldn't be used.
@@ -33,6 +34,7 @@ export function postId( state = null, action ) { | |||
export default combineReducers( { | |||
postId, | |||
imageEditor, | |||
videoEditor, |
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 appears like the video editor state has no reference to any particular media id or site or anything. it's "whatever is out front in the editor" but what happens if we change sites or change videos which we are working on? won't we have race-conditions with network requests on the wire?
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.
The video editor is a modal that is opened from the media library modal. Media library modal handles passing the particular video and everything else the video editor needs to know. And because it's a modal, site switching should be a non-issue. Hope that helps clarify and makes sense.
*/ | ||
export function hasPosterUpdateError( state ) { | ||
return state.ui.editor.videoEditor.hasPosterUpdateError; | ||
} |
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.
please note that we are putting all new selectors into the flat state/selectors
directory as part of a new search-based selector system (check the devdocs). they belong one for each file where the function name matches the file name except for casing
// has-poster-update-error.js
export default function hasPosterUpdateError( state ) {
…
}
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.
among other things - the selectors definitely need to move into the new flat selectors directory
@dmsnell The ball's back in your court! |
switch ( action.type ) { | ||
case VIDEO_EDITOR_SET_POSTER_URL: | ||
case VIDEO_EDITOR_SHOW_ERROR: | ||
return true; |
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 the poster really updated if we got an error? seems like a semantic mismatch between the data point and its use
* Updates the poster for a video. | ||
* | ||
* @param {Object} store Redux store | ||
* @param {Object} action Action 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.
nitpick: we have extra spaces here
return next( action ); | ||
} | ||
|
||
const { atTime, file } = pick( action.params, [ 'atTime', 'file' ] ); |
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 pick
is necessary here as the destructuring should already do exactly what we want…
const { atTime, file } = action.params;
}; | ||
|
||
export const updatePosterError = ( { dispatch } ) => { | ||
dispatch( showError() ); |
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 error concept is still bugging me. we don't actually show an error do we? it seems like all we really do is indicate that the poster is no longer updating. is the error coming later?
is there another way to represent the poster URL which would indicate if it's reset or set? on error, instead of sending an error action, would it make sense to revert the URL to its previous state, or an empty string? if so, we could instead dispatch something like setPosterUrl( previousURL )
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.
We do actually surface an error message to the user in the case where the poster could not be updated. To be clear, the new poster URL gets generated server-side and is sent back to the client on a successful update. If the poster updates without issue, the video editor modal closes and goes back to the Media Library modal, which will load the VideoPress player with the new poster showing.
In the event of an error occurring when updating a poster, we're not in a situation where the poster URL needs to be reverted because it was never changed in the first place.
As for the RESET_STATE action, that's there to clear out any existing state when opening the video editor modal again. Otherwise you might open it and see an error message if your first attempt at updating the poster wasn't successful.
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.
We do actually surface an error message to the user
did I miss this? where does that happen?
}; | ||
|
||
export const updatePosterUrl = ( { dispatch }, action, next, { poster } ) => { | ||
dispatch( setPosterUrl( poster ) ); |
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 poster
is the name of the value coming from the API? it might help clarity here to rename in the arguments because it feels like poster
ought to be an object here instead of a URL string…
…, next, { poster: posterURL } ) => {
export const uploadProgress = ( state = 0, action ) => { | ||
switch ( action.type ) { | ||
case VIDEO_EDITOR_SHOW_UPLOAD_PROGRESS: | ||
return action.percentage; |
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 advice I'd share from the importer is that in some cases 100% is a value we never really see and it might be beneficial to fib a bit here. since 100% came at the very end, or never came because the load
event occurred after something close like 96% I made it look like 100% names at an early value and this meant that people would see a positive confirmation that the processes was ending vs going from processing to oops I missed the end.
this can also (maybe more appropriately) be handled in the UI
@donnapep I'm still a bit leery about whether or not we need specific ERROR actions or even the RESET action. I think you said more error handling was on its way later, so please disregard if I'm overlooking that. on the reset, what is a reset other than setting the URL to const setUrl = url => ( { type: SET_URL, url } );
const resetUrl = () => setUrl( '' );
const url = ( state = null, { type, url } ) =>
SET_URL === type
? url
: state;
const percentage = ( state = null, { type, percentage, url } ) => {
if ( UPDATE === type ) {
return percentage;
}
if ( SET_URL === type && ! url ) {
return 0;
}
return state;
} |
After the poster is successfully updated, an action to set the poster URL is dispatched, followed by a separate action to indicate that the video editor modal should close. The `shouldCloseVideoEditorModal` selector replaces the `isPosterUpdated` selector, and uses better naming to more accurately reflect its purpose.
In particular, VIDEO_EDITOR_CLOSE_MODAL can be used to reset the upload progress so that if the modal is reopened, the UI is correct (i.e. no progress bar showing and buttons are enabled).
@dmsnell I've made some changes to the Redux actions and was able to eliminate the I did some other refactoring and, thanks to my amazing image annotation abilities, hereby present to you the following screen captures that hopefully help to better illustrate why each action is necessary and how it is used. There are 5 actions added by the video editor, namely:
|
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.
@donnapep you did lots of work on this and the PR is in a very nice condition. I'm happy with you merging it in; very good work!
@dmsnell 🎉 Once again, thank you for your infinite patience and wisdom! |
This PR separates out the Redux state changes from PR #11389, which adds support for selecting or uploading a custom image to serve as the poster for a video.
Post discussing Calypso-based VideoPress poster chooser can be found at pxWta-JO-p2 (internal ref).
Upload poster endpoint