Skip to content

Commit

Permalink
Merge pull request #5652 from Automattic/add/reader/start-load-new-re…
Browse files Browse the repository at this point in the history
…commendation-on-card-interaction

Reader: load a new recommendation after an interaction with a card
  • Loading branch information
bluefuton authored Jun 13, 2016
2 parents 0a02c78 + f539fd8 commit 2e08e6a
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 54 deletions.
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
};

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

0 comments on commit 2e08e6a

Please sign in to comment.