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

Reader: load a new recommendation after an interaction with a card #5652

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { bindActionCreators } from 'redux';
/**
* Internal dependencies
*/
import { isRequestingRecommendations, getRecommendationsInteractedWith } from 'state/reader/start/selectors';
import { isRequestingRecommendations } from 'state/reader/start/selectors';
import { requestRecommendations } from 'state/reader/start/actions';

class QueryReaderStartRecommendations extends Component {
Expand All @@ -17,7 +17,7 @@ class QueryReaderStartRecommendations extends Component {
return;
}

this.props.requestRecommendations( this.props.recommendationsInteractedWith );
this.props.requestRecommendations();
}

render() {
Expand All @@ -37,8 +37,7 @@ QueryReaderStartRecommendations.defaultProps = {
export default connect(
( state ) => {
return {
isRequestingRecommendations: isRequestingRecommendations( state ),
recommendationsInteractedWith: getRecommendationsInteractedWith( state )
isRequestingRecommendations: isRequestingRecommendations( state )
};
},
( dispatch ) => {
Expand Down
4 changes: 3 additions & 1 deletion client/reader/start/card-footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ const StartCardFooter = ( { site } ) => {
const subscribersCount = numberFormat( site.subscribers_count );
return (
<footer className="reader-start-card-footer">
<div className="reader-start-card__follower-count">{ subscribersCount } followers</div>
<div className="reader-start-card__follower-count">
<a href={ `/read/blogs/${site.ID}` }>{ subscribersCount } followers</a>
</div>
<FollowButton siteUrl={ site.URL } />
</footer>
);
Expand Down
8 changes: 5 additions & 3 deletions client/reader/start/card-header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import { getSite } from 'state/reader/sites/selectors';
const StartCardHeader = ( { site } ) => {
return (
<header className="reader-start-card__header">
<SiteIcon site={ site } size={ 70 } />
<h1 className="reader-start-card__site-title">{ site.title }</h1>
<p className="reader-start-card__site-description">{ site.description }</p>
<a href={ `/read/blogs/${site.ID}` }>
<SiteIcon site={ site } size={ 70 } />
<h1 className="reader-start-card__site-title">{ site.title }</h1>
<p className="reader-start-card__site-description">{ site.description }</p>
</a>
</header>
);
};
Expand Down
54 changes: 36 additions & 18 deletions client/reader/start/card.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// External dependencies
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import debugModule from 'debug';
import get from 'lodash/get';
import classnames from 'classnames';
Expand All @@ -11,27 +12,40 @@ import StartPostPreview from './post-preview';
import StartCardHero from './card-hero';
import StartCardHeader from './card-header';
import StartCardFooter from './card-footer';
import { recordRecommendationInteraction } from 'state/reader/start/actions';
import { getRecommendationById } from 'state/reader/start/selectors';

const debug = debugModule( 'calypso:reader:start' ); //eslint-disable-line no-unused-vars

const StartCard = ( { siteId, postId } ) => {
const cardClasses = classnames(
'reader-start-card',
{
'has-post-preview': ( postId > 0 )
}
);

return (
<Card className={ cardClasses }>
<StartCardHero siteId={ siteId } postId={ postId } />
<StartCardHeader siteId={ siteId } />
{ postId > 0 && <StartPostPreview siteId={ siteId } postId={ postId } /> }
<StartCardFooter siteId={ siteId } />
</Card>
);
};
const StartCard = React.createClass( {
onCardInteraction() {
this.props.recordRecommendationInteraction(
this.props.recommendationId,
this.props.recommendation.recommended_site_ID,
this.props.recommendation.recommended_post_ID
);
},

render() {
const { siteId, postId } = this.props;

const cardClasses = classnames(
'reader-start-card',
{
'has-post-preview': ( postId > 0 )
}
);

return (
<Card className={ cardClasses } onClick={ this.onCardInteraction }>
<StartCardHero siteId={ siteId } postId={ postId } />
<StartCardHeader siteId={ siteId } />
{ postId > 0 && <StartPostPreview siteId={ siteId } postId={ postId } /> }
<StartCardFooter siteId={ siteId } />
</Card>
);
}
} );

StartCard.propTypes = {
recommendationId: React.PropTypes.number.isRequired
Expand All @@ -44,8 +58,12 @@ export default connect(
const postId = get( recommendation, 'recommended_post_ID' );

return {
recommendation,
siteId,
postId
};
}
},
( dispatch ) => bindActionCreators( {
recordRecommendationInteraction
}, dispatch )
)( StartCard );
9 changes: 8 additions & 1 deletion client/reader/start/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@
}
}

.reader-start-card__site-title {
color: $gray-dark;
}

.reader-start-card__site-title,
.reader-start-card__site-description {
text-align: center;
Expand All @@ -196,6 +200,10 @@
font-weight: 600;
margin-top: 20px;

a {
color: inherit;
}

@include breakpoint( ">480px" ) {
margin-top: 30px;
}
Expand Down Expand Up @@ -302,7 +310,6 @@
}

@keyframes slideout-bar {

0% {
transform: translateY( calc( 100vh - 100px ) );
}
Expand Down
1 change: 1 addition & 0 deletions client/state/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ export const READER_LISTS_UNFOLLOW_FAILURE = 'READER_LISTS_UNFOLLOW_FAILURE';
export const READER_POSTS_RECEIVE = 'READER_POSTS_RECEIVE';
export const READER_SIDEBAR_LISTS_TOGGLE = 'READER_SIDEBAR_LISTS_TOGGLE';
export const READER_SIDEBAR_TAGS_TOGGLE = 'READER_SIDEBAR_TAGS_TOGGLE';
export const READER_START_RECOMMENDATION_INTERACTION = 'READER_START_RECOMMENDATION_INTERACTION';
export const READER_START_RECOMMENDATIONS_RECEIVE = 'READER_START_RECOMMENDATIONS_RECEIVE';
export const READER_START_RECOMMENDATIONS_REQUEST = 'READER_START_RECOMMENDATIONS_REQUEST';
export const READER_START_RECOMMENDATIONS_REQUEST_SUCCESS = 'READER_START_RECOMMENDATIONS_REQUEST_SUCCESS';
Expand Down
51 changes: 47 additions & 4 deletions client/state/reader/start/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import map from 'lodash/map';
import omit from 'lodash/omit';
import property from 'lodash/property';
import debugModule from 'debug';

/**
* Internal dependencies
Expand All @@ -13,11 +14,17 @@ import {
READER_START_RECOMMENDATIONS_RECEIVE,
READER_START_RECOMMENDATIONS_REQUEST,
READER_START_RECOMMENDATIONS_REQUEST_SUCCESS,
READER_START_RECOMMENDATIONS_REQUEST_FAILURE
READER_START_RECOMMENDATIONS_REQUEST_FAILURE,
READER_START_RECOMMENDATION_INTERACTION
} from 'state/action-types';
import { updateSites } from 'state/reader/sites/actions';
import { receivePosts } from 'state/reader/posts/actions';

/**
* Module variables
*/
const debug = debugModule( 'calypso:redux:reader-start-recommendations' );

/**
* Returns an action object to signal that recommendation objects have been received.
*
Expand All @@ -31,18 +38,54 @@ export function receiveRecommendations( recommendations ) {
};
}

/**
* Returns an action object to signal that a recommendation has been interacted with.
*
* @param {Integer} recommendationId Recommendation ID
* @param {Integer} siteId Site ID
* @param {Integer} postId Post ID
* @return {Function} Action thunk
*/
export function recordRecommendationInteraction( recommendationId, siteId, postId ) {
return( dispatch ) => {
debug( 'User interacted with recommendation ' + recommendationId );
const numberOfRecommendationsToLoad = 3;
dispatch( requestRecommendations( siteId, postId, numberOfRecommendationsToLoad ) );
dispatch( {
type: READER_START_RECOMMENDATION_INTERACTION,
recommendationId
} );
};
}

/**
* Triggers a network request to fetch recommendations.
*
* @return {Function} Action thunk
* @param {Integer} originSiteId Origin site ID
* @param {Integer} originPostId Origin post ID
* @param {Integer} limit Maximum number of results to return
* @return {Function} Action thunk
*/
export function requestRecommendations() {
export function requestRecommendations( originSiteId, originPostId, limit ) {
return ( dispatch ) => {
dispatch( {
type: READER_START_RECOMMENDATIONS_REQUEST,
} );

return wpcom.undocumented().readRecommendationsStart( { meta: 'site,post' } )
if ( ! limit ) {
limit = 20;
}

const query = {
meta: 'site,post',
origin_site_ID: originSiteId,
origin_post_ID: originPostId,
number: limit
Copy link
Contributor

@TooTallNate TooTallNate May 29, 2016

Choose a reason for hiding this comment

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

number doesn't currently do anything in the API endpoint FYI.

The LIMIT in the SQL query is currently hard-coded to 20.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

number= is supported now (thanks for adding @TooTallNate!)

};

debug( 'Requesting recommendations for site ' + originSiteId + ' and post ' + originPostId );

return wpcom.undocumented().readRecommendationsStart( query )
.then( ( data ) => {
// Collect sites and posts from meta, and receive them separately
const sites = map( data.recommendations, property( 'meta.data.site' ) );
Expand Down
50 changes: 37 additions & 13 deletions client/state/reader/start/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
* External dependencies
*/
import { combineReducers } from 'redux';
import keyBy from 'lodash/keyBy';
import union from 'lodash/union';
import find from 'lodash/find';
import filter from 'lodash/filter';

/**
* Internal dependencies
Expand All @@ -12,31 +14,39 @@ import {
READER_START_RECOMMENDATIONS_REQUEST,
READER_START_RECOMMENDATIONS_REQUEST_SUCCESS,
READER_START_RECOMMENDATIONS_REQUEST_FAILURE,
READER_START_RECOMMENDATION_INTERACTION,
SERIALIZE,
DESERIALIZE,
} from 'state/action-types';
import { itemsSchema } from './schema';
import { isValidStateWithSchema } from 'state/utils';

/**
* Tracks all known list objects, indexed by list ID.
*
* @param {Object} state Current state
* @param {Array} state Current state
* @param {Object} action Action payload
* @return {Object} Updated state
* @return {Array} Updated state
*/
export function items( state = {}, action ) {
export function items( state = [], action ) {
switch ( action.type ) {
case READER_START_RECOMMENDATIONS_RECEIVE:
return Object.assign( {}, state, keyBy( action.recommendations, 'ID' ) );
// Filter out any recommendations we already have
const newRecommendations = filter( action.recommendations, ( incomingRecommendation ) => {
return ! find( state, { ID: incomingRecommendation.ID } );
} );

// No new recommendations? Just return the existing ones.
if ( newRecommendations.length < 1 ) {
return state;
}

return state.concat( newRecommendations );

// Always return default state - we don't want to serialize recommendations
case SERIALIZE:
return state;
case DESERIALIZE:
if ( ! isValidStateWithSchema( state, itemsSchema ) ) {
return {};
}
return state;
return [];
}

return state;
}

Expand All @@ -62,7 +72,21 @@ export function isRequestingRecommendations( state = false, action ) {
return state;
}

export function recommendationsInteractedWith( state = [], action ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

is the order of interaction important? Could this just be a Set?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not in the UI, but it's pretty useful for debugging at the moment :) I'll leave it as an array for now.

switch ( action.type ) {
case READER_START_RECOMMENDATION_INTERACTION:
return union( state, [ action.recommendationId ] );

case SERIALIZE:
case DESERIALIZE:
return [];
}

return state;
}

export default combineReducers( {
items,
isRequestingRecommendations
isRequestingRecommendations,
recommendationsInteractedWith
} );
3 changes: 0 additions & 3 deletions client/state/reader/start/schema.js

This file was deleted.

Loading