-
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
Gravatar: Show current user's local image #9369
Conversation
@@ -6,7 +6,7 @@ var React = require( 'react' ); | |||
/** | |||
* Internal dependencies | |||
*/ | |||
var Gravatar = require( 'components/gravatar' ); | |||
var Gravatar = require( 'components/gravatar' ).default; |
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 change this to an import usage. This protects us against breakages if components/gravatar
goes back to exporting 1 thing.
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.
Agreed, that's a better plan :) Updated all the components to use import
instead.
} | ||
|
||
export default connect( ( state, ownProps ) => { | ||
const userId = ownProps.user && ownProps.user.ID; |
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.
Optional:
get( ownProps, 'user.id', 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.
That's neat, I'm using it now. Didn't use false
as the fallback since we're expecting undefined
anyway.
this.setState( { failedToLoad: 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.
Needs a semicolon
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.
Missed this one earlier, fixed now.
@@ -18,7 +18,7 @@ const Card = require( 'components/card' ), | |||
FormButtonsBar = require( 'components/forms/form-buttons-bar' ), | |||
AuthorSelector = require( 'blocks/author-selector' ), | |||
UsersActions = require( 'lib/users/actions' ), | |||
Gravatar = require( 'components/gravatar' ), | |||
Gravatar = require( 'components/gravatar' ).default, |
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 add an Gravatar import after the requires. We can go back to clean up the rest of the requires in a follow up janitorial PR
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.
👍
@@ -6,7 +6,7 @@ const React = require( 'react' ); | |||
/** | |||
* Internal dependencies | |||
*/ | |||
const Gravatar = require( 'components/gravatar' ), | |||
const Gravatar = require( 'components/gravatar' ).default, |
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.
imports seems small enough in this file to convert all of them
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.
👍
224b054
to
de6c9e0
Compare
6c06b1c
to
d9c0e7e
Compare
Updated & rebased branch. |
adcbea7
to
022b060
Compare
👍 Verified that only usage of |
👍 With the feature on, my gravatar is updated instantly, and within 5 min, if I refresh it will correctly show the gravatar src vs the local cache. |
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 work!
The code is working fine (I didn't find any regressions). However, I added a few smaller things/ideas. Could you please look at them?
export class Gravatar extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
this.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.
Instead of having to write constructor just to set the initial state, I discovered that we can use ES7 static properties to define the initial state as well. This is how:
export class Gravatar extends Component {
state = {
failedToLoad: false
};
// ... other code follows
Note: I'm not 100% sure it'll work, actually. Haven't tried myself. :)
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 mean property initializers right, since we don't want this to be static. 😄 I'm fine with the above usage
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.
Leaving as-is for now.
|
||
propTypes: { | ||
static displayName = 'Gravatar'; |
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.
Do we need this for the case when require
is used instead of import
?
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 missed this, but displayName is added in a babel transform, so this isn't necessary here unless we want one that has a different value from the class name
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.
👍 good catch, I removed it.
propTypes: { | ||
static displayName = 'Gravatar'; | ||
|
||
static propTypes = { | ||
user: React.PropTypes.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.
Could we import PropTypes
directly so we don't have to write React.
before 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.
Sounds good! Imported PropTypes
.
}, | ||
imgSize: React.PropTypes.number, | ||
// connected props: | ||
tempImage: React.PropTypes.oneOfType( [ |
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.
Why can be the tempImage
both a string and a bool? We could maybe add some inline comment to clarify further. :)
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.
Sure thing, added comments to clarify.
failedToLoad: false | ||
}; | ||
}, | ||
imgSize: 96, |
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 in px
? If yes, could we add it as an inline comment, please?
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.
Hmm! Not really sure about adding a comment here since it's not obvious at first glance what imgSize
and size
are there for, and if they both have a px
comment it may get more confusing. Both of these are described in the README - is that enough to go on, what do you think @lamosty?
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 it's in README (sorry for not noticing), it's good enough. :)
const avatarURL = this.getResizedImageURL( safeImageURL( this.props.user.avatar_URL ) ); | ||
|
||
const avatarURL = this.props.tempImage || | ||
this.getResizedImageURL( safeImageURL( this.props.user.avatar_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.
Could we extract these props out of props
at the top of the function? Like:
const {
alt,
user,
tempImage,
size
// etc...
} = this.props;
Also, shouldn't we write a new selector for getting the user's avatar_URL
instead of accessing it directly like user.avatar_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.
Could we extract these props out of props
Yes :) I extracted the props in render
and also in getResizedImageURL
.
shouldn't we write a new selector for getting the user's avatar_URL
Do you mean a new redux selector? If so, some components use their own user objects, not relying on the users in state.users
, so they won't be able to pass in an ID and let <Gravatar />
get the selected user's info. For example, ReaderAvatar
.
|
||
export default connect( ( state, ownProps ) => { | ||
return { | ||
tempImage: getUserTempGravatar( state, get( ownProps, 'user.ID' ) ), |
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 could maybe default to the ID of the current user in get( ownProps, 'user.ID' )
if user
object is not provided in ownProps
.
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.
Do you mean the user passed in to the <Gravatar />
component? If that's the case, we don't always have an ID or a user object to rely on. I'd rather not complicate the logic, since getUserTempGravatar
expects a missing ID if we don't have it, so I think it's ok not to provide anything in that case.
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.
Okay, I see.
The idea I have is to return some default value from the get
helper fn if user.ID
is not found in ownProps
. It would be a bit easier to reason about if I saw the default value there.
In case getUserTempGravatar
accepts false
as the userId
, we can just add it like:
tempImage: getUserTempGravatar( state, get( ownProps, 'user.ID', 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.
Ok, I can dig adding a default value instead of relying on undefined
for readability. Updated it here, and added a test.
@@ -18,9 +18,9 @@ const Card = require( 'components/card' ), | |||
FormButtonsBar = require( 'components/forms/form-buttons-bar' ), | |||
AuthorSelector = require( 'blocks/author-selector' ), | |||
UsersActions = require( 'lib/users/actions' ), | |||
Gravatar = require( 'components/gravatar' ), | |||
accept = require( 'lib/accept' ), | |||
analytics = require( 'lib/analytics' ); |
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 add a blank line below so old require
s and new import
s are separated. :)
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 tried it out with a new line, and the extra space makes it seem like <Gravatar />
is not an internal dependency, so I guess it's a bit strange either way. It looks ok to me without the extra line since the import
keyword is in a new block-ish, so thinking of leaving as is.
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 should just follow up by porting the rest of the requires to imports in a janitorial PR. Spacing isn't a huge deal for now.
022b060
to
8a9ca8c
Compare
Updated, added README section, and rebased. |
Feedback looks good. Let's put larger changes such as new selectors into a follow up PRs
import PostActions from 'lib/posts/actions'; | ||
import touchDetect from 'lib/touch-detect'; | ||
import sitesFactory from 'lib/sites-list'; | ||
import stats from 'lib/posts/stats'; |
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.
Whoops, this one is easy to miss. So either
import { recordStat, recordEvent } from 'lib/posts/stats';
//plus update usages below
or
import * as stats from 'lib/posts/stats';
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.
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 did a quick audit of the other imports, and they 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.
Updated to import * as stats from 'lib/posts/stats';
. All other imports looks ok to me, too.
8ab9560
to
31036a1
Compare
expectedResultString = '<img alt="Bob The Tester" class="gravatar" ' + | ||
'src="https://0.gravatar.com/' + | ||
'avatar/cf55adb1a5146c0a11a808bce7842f7b?s=96&d=mm" ' + | ||
'width="32" height="32"/>'; |
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 use one const
or let
per variable please 😄
expectedResultString = '<img alt="Bob The Tester" class="gravatar" ' + | ||
'src="https://0.gravatar.com/' + | ||
'avatar/cf55adb1a5146c0a11a808bce7842f7b?s=96&d=mm" ' + | ||
'width="32" height="32"/>'; | ||
|
||
assert.equal( expectedResultString, stripReactAttributes( ReactDomServer.renderToStaticMarkup( gravatar ) ) ); |
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.
@v18 I highly recommend you check out enzyme()
for testing. it can make testing React components significantly easier.
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 - these tests were written before my time. I'm not sure that updating the tests for this PR falls under the scope here :) The tests I wrote for a different PR use enzyme <3
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.
@v18 as you will. I would hate to add more cruft if it's not too much more work to update there. wouldn't have to change the whole 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.
@dmsnell I know what you mean :) On the other hand, to change the tests, I'd need to rework the entire file, no? They all rely on checking the final HTML output vs checking properties.
@v18 why are we assigning a temporary src and providing selectors for it? |
This is so when you update a gravatar on /me, you see the change immediately. Otherwise it will take another 5 minutes before the gravatar img cache expires. |
how does this matter to Calypso though? if we change the avatar in our state, why does the gravatar cache matter? |
@dmsnell - so when we request an image, Gravatar sends back the response with a cache directive of 5 minutes. This means that even when the image is updated on Gravatar.com, the browser won't refetch it. It's a bit more complicated since Chrome will actually not refetch it even after 5 minutes. For the user this means that the image won't show up immediately, which is :sadxmas:, so we display the local image we already have once it's uploaded for a smooth experience. I went over some more details in #9212. |
31036a1
to
da655a5
Compare
Updated with above comments, & rebased. |
this is indeed sad, but do we need the temp image? if we're going to be using it, why not simply store the uploaded "temporary" image as the de-facto real gravatar image and forget about the "temp" stuff. it will only persist through the current browsing session anyway (unless the user uploads another one). on the other hand, the "temporary" image won't persist either, will it? won't we need to grab the Gravatar URL on each actual Calypso load? |
I'm not sure I understand what you mean here. Store the uploaded "temporary" image where?
Yes, that's correct. In fact we fetch Gravatars many times through the lifecycle of one browsing session. This is probably true even if is less than 5 minutes, since we use many Gravatar sizes. |
instead of doing gravatarUrl = buildGravatarUrl( email )
tempAvatarUrl = uploadedImageUrl do gravatarUrl = uploadedImageUrl
are we doing multiple fetches for any given email? why would we do that? if we are, it seems like it would be worthwhile to store the returned Gravatar URL in state and not fetch it again if we already have it. we could surely get out of sync with Gravatar, but within a single session is probably more than sufficient. |
does the temporary URL work as-is even when requesting multiple sizes? |
It's nice user feedback that something happened. I suggest you walk through steps in #9317 The difference being that at the end of the flow you see the new image rather than the old gravatar
Which is fine since Gravatar should be the source of truth |
@dmsnell Yes, exactly, we are doing multiple fetches. The reason is because we sometimes request Gravatars of different sizes. Also, after the 5 minute cache is up, the browser should be re-fetching the cached result of the previous fetch, given browser specs. I agree with you, there are definitely a bunch of unneeded network requests going on. I'm not 100% sure yet whether tackling Gravatar network requests is of one of the highest priorities in Calypso, even in terms of performance work. I think that after the new uploading Gravatar feature is done, it would be worthwhile to examine if it's a good road to go under given other priorities.
Yes. The temporary image is locally stored as a base64 string, so we're not doing network requests to get different sizes. |
There's already a browser cache of 5 minutes. I don't think it's worthwhile to have application state deal with caching and invalidating this for other users whose gravatars won't change For the uploaded image we went for the simple approach: |
okay @gwwar and @v18 this is all fine. based on what I'm reading, I think we could have gotten away without creating a bunch of if we're storing the temp image and know when to stop using it, we could just as easily store the temp image as truth and know when to stop using it as well. just a little nudge that things aren't always as inherently complex as they may seem. |
well, we are explicitly stating here that it shouldn't be (by means of the temp image). this is why I advocate so strongly for designing Calypso such that our app state is the source of truth and any changes from the outside should come in through a middleware layer - trust the framework to manage the data. |
Polling the user object would overwrite any temp images. Knowing what values to keep is difficult without adding complexity |
da655a5
to
383fa5e
Compare
Updated the branch. All e2e pass, and all the manual tests are working. |
Let's |
The changes in this PR:
<Gravatar />
becomes a connected component that usesgetUserTempGravatar
<Gravatar />
exportsdefault
andGravatar
. The 3 components that userequire
to use<Gravatar />
now use the default export<Gravatar />
to an ES6 classWhat we're testing
In each case, test to make sure that the
<Gravatar/>
on the page shows up, and that there are no errors in the console.Manual Testing
The three components that use
require
to use<Gravatar />
are:components/user
(UserItem
)This can be tested at
http://calypso.localhost:3000/settings/import/$wpcom_site
by going through an importmy-sites/people/delete-user
(DeleteUser
)This cannot be tested right now since the component seems to not be working atm (not allowing to reassign posts to a user). The link to test is here:
http://calypso.localhost:3000/people/edit/editor/$jetpack_site
- try deleting a userpost-editor/editor-author
(EditorAuthor
)This can be tested at the editor:
http://calypso.localhost:3000/post/$site
The following components use
import
to use<Gravatar />
, and did not need to be changed, sorted by URL:A:
http://calypso.localhost:3000/read/feeds/45401477/posts/1216523546#comments
reader/comments/form.jsx
- try to leave a commentreader/comments/post-comment.jsx
- read the comments already thereblocks/reader-avatar
(ReaderAvatar
)blocks/reader-related-card-v2
(RelatedPostCard
)B:
http://calypso.localhost:3000/posts/my/$site
blocks/comments/form.jsx
(PostCommentForm
) - leave a commentblocks/comments/post-comment
- view commentsmy-sites/posts/posts-navigation
- the navigation bar at the top of the page: published/me/everyoneC:
http://calypso.localhost:3000/me
layout/masterbar/logged-in
- the masterbarme/sidebar-navigation/sidebar-navigation.jsx
me/profile-gravatar
EditGravatar
D:
http://calypso.localhost:3000/
reader/post-byline
E:
http://calypso.localhost:3000/people/team/$site
my-sites/people/people-profile
F:
http://calypso.localhost:3000/posts/$site
my-sites/draft/index.jsx
click on 'everyone' to see the drafts list with GravatarsG:
http://calypso.localhost:3000/read/a8c
reader/site-and-author-icon/
- find an x-postH:
http://calypso.localhost:3000/devdocs/blocks/search-card
blocks/reader-author-and-site
(AuthorAndSite
) (not in prod)I:
http://calypso.localhost:3000/recommendations/start
reader/start/post-preview.jsx
J:
http://calypso.localhost:3000/jetpack/connect
signup/jetpack-connect/authorize-form.jsx
enter a .org URL to connect to JPK: user invitation link
my-sites/invites/invite-header
my-sites/invites/invite-accept-logged-in
L:
https://wpcalypso.wordpress.com/devdocs/blocks/happiness-support
components/happiness-support
(HappinessSupport
)M:
http://calypso.localhost:3000/help
me/help/help-happiness-engineers
N: Component:
signup/jetpack-connect/sso.jsx
. Steps:calypso.localhost:3000
in the URL<Gravatar />
and no errorse2e tests
For the original PR (#8456) that attempted to make similar changes there were some failing e2e tests. All of the e2e tests pass, except these 3: