diff --git a/client/reader/sidebar/index.jsx b/client/reader/sidebar/index.jsx index faee472c6c34b..a4b62fc01b951 100644 --- a/client/reader/sidebar/index.jsx +++ b/client/reader/sidebar/index.jsx @@ -1,6 +1,5 @@ import { isEnabled } from '@automattic/calypso-config'; import page from '@automattic/calypso-router'; -import { hasTranslation } from '@wordpress/i18n'; import closest from 'component-closest'; import i18n, { localize } from 'i18n-calypso'; import { defer, startsWith } from 'lodash'; @@ -20,7 +19,6 @@ import SidebarSeparator from 'calypso/layout/sidebar/separator'; import ReaderA8cConversationsIcon from 'calypso/reader/components/icons/a8c-conversations-icon'; import ReaderConversationsIcon from 'calypso/reader/components/icons/conversations-icon'; import ReaderDiscoverIcon from 'calypso/reader/components/icons/discover-icon'; -import ReaderFollowingIcon from 'calypso/reader/components/icons/following-icon'; import ReaderLikesIcon from 'calypso/reader/components/icons/likes-icon'; import ReaderManageSubscriptionsIcon from 'calypso/reader/components/icons/manage-subscriptions-icon'; import ReaderNotificationsIcon from 'calypso/reader/components/icons/notifications-icon'; @@ -117,13 +115,6 @@ export class ReaderSidebar extends Component { } ); }; - handleReaderSidebarFollowedSitesClicked = ( event, path ) => { - recordAction( 'clicked_reader_sidebar_followed_sites' ); - recordGaEvent( 'Clicked Reader Sidebar Followed Sites' ); - this.props.recordReaderTracksEvent( 'calypso_reader_sidebar_followed_sites_clicked' ); - this.handleGlobalSidebarMenuItemClick( path ); - }; - handleReaderSidebarConversationsClicked = ( event, path ) => { recordAction( 'clicked_reader_sidebar_conversations' ); recordGaEvent( 'Clicked Reader Sidebar Conversations' ); @@ -174,8 +165,8 @@ export class ReaderSidebar extends Component { }; renderSidebarMenu() { - const { path, translate, teams, locale } = this.props; - const recentLabelTranslationReady = hasTranslation( 'Recent' ) || locale.startsWith( 'en' ); + const { path, translate, teams } = this.props; + return ( @@ -194,25 +185,13 @@ export class ReaderSidebar extends Component { - { isEnabled( 'reader/recent-feed-overhaul' ) ? ( -
  • - -
  • - ) : ( - } - link="/read" +
  • + - ) } +
  • ( ( state ) => state.readerUi.sidebar.selectedRecentSite ); + const recordReaderTracksEvent = useRecordReaderTracksEvent(); const dispatch = useDispatch(); let sitesToShow = showAllSites ? sites : sites.slice( 0, SITE_DISPLAY_CUTOFF ); @@ -75,6 +78,17 @@ const ReaderSidebarRecent = ( { if ( ! RECENT_PATH_REGEX.test( path ) ) { page( '/read' ); } + + // Analytics. + if ( feedId ) { + recordAction( 'clicked_reader_sidebar_followed_single_site' ); + recordGaEvent( 'Clicked Reader Sidebar Followed Single Site' ); + recordReaderTracksEvent( 'calypso_reader_sidebar_followed_single_site_clicked' ); + } else { + recordAction( 'clicked_reader_sidebar_followed_sites' ); + recordGaEvent( 'Clicked Reader Sidebar Followed Sites' ); + recordReaderTracksEvent( 'calypso_reader_sidebar_followed_sites_clicked' ); + } }; return ( diff --git a/client/reader/stream/index.jsx b/client/reader/stream/index.jsx index 1764ce9f94c69..c9ef7ad564ca0 100644 --- a/client/reader/stream/index.jsx +++ b/client/reader/stream/index.jsx @@ -29,6 +29,7 @@ import { getReaderOrganizations } from 'calypso/state/reader/organizations/selec import { getPostByKey } from 'calypso/state/reader/posts/selectors'; import { getBlockedSites } from 'calypso/state/reader/site-blocks/selectors'; import { + clearStream, requestPage, selectItem, selectNextItem, @@ -42,6 +43,7 @@ import { } from 'calypso/state/reader/streams/selectors'; import { viewStream } from 'calypso/state/reader-ui/actions'; import { resetCardExpansions } from 'calypso/state/reader-ui/card-expansions/actions'; +import { getSelectedFeedId } from 'calypso/state/reader-ui/sidebar/selectors'; import getCurrentLocaleSlug from 'calypso/state/selectors/get-current-locale-slug'; import getPrimarySiteId from 'calypso/state/selectors/get-primary-site-id'; import isNotificationsOpen from 'calypso/state/selectors/is-notifications-open'; @@ -126,8 +128,13 @@ class ReaderStream extends Component { this.setState( { selectedTab: 'sites' } ); }; - componentDidUpdate( { selectedPostKey, streamKey } ) { - if ( streamKey !== this.props.streamKey ) { + componentDidUpdate( { selectedPostKey, streamKey, selectedFeedId } ) { + // Fetch new page if selected feed or stream is changed. + if ( selectedFeedId !== this.props.selectedFeedId ) { + this.scrollFeedListToTop(); + this.props.clearStream( { streamKey } ); + this.fetchNextPage( {}, { ...this.props, stream: null } ); // Stream as null to start fresh pagination. + } else if ( streamKey !== this.props.streamKey ) { this.props.resetCardExpansions(); this.props.viewStream( streamKey, window.location.pathname ); this.fetchNextPage( {} ); @@ -147,6 +154,7 @@ class ReaderStream extends Component { if ( this.props.shouldRequestRecs ) { this.props.requestPage( { streamKey: this.props.recsStreamKey, + feedId: this.props.selectedFeedId, pageHandle: this.props.recsStream.pageHandle, localeSlug: this.props.localeSlug, } ); @@ -447,8 +455,13 @@ class ReaderStream extends Component { }; poll = () => { - const { streamKey, localeSlug } = this.props; - this.props.requestPage( { streamKey, isPoll: true, localeSlug: localeSlug } ); + const { streamKey, localeSlug, selectedFeedId } = this.props; + this.props.requestPage( { + streamKey, + feedId: selectedFeedId, + isPoll: true, + localeSlug: localeSlug, + } ); }; getPageHandle = ( pageHandle, startDate ) => { @@ -461,28 +474,30 @@ class ReaderStream extends Component { }; fetchNextPage = ( options, props = this.props ) => { - const { streamKey, stream, startDate, localeSlug } = props; + const { streamKey, stream, startDate, localeSlug, selectedFeedId } = props; if ( options.triggeredByScroll ) { const pageId = pagesByKey.get( streamKey ) || 0; pagesByKey.set( streamKey, pageId + 1 ); props.trackScrollPage( pageId ); } - const pageHandle = this.getPageHandle( stream.pageHandle, startDate ); - props.requestPage( { streamKey, pageHandle, localeSlug } ); + const pageHandle = stream ? this.getPageHandle( stream.pageHandle, startDate ) : null; + props.requestPage( { feedId: selectedFeedId, streamKey, pageHandle, localeSlug } ); }; showUpdates = () => { const { streamKey } = this.props; this.props.onUpdatesShown(); this.props.showUpdates( { streamKey } ); - // @todo: do we need to shuffle? - // if ( this.props.recommendationsStore ) { - // shufflePosts( this.props.recommendationsStore.id ); - // } - if ( this.listRef.current ) { - this.listRef.current.scrollToTop(); + this.scrollFeedListToTop(); + }; + + scrollFeedListToTop = () => { + if ( ! this.listRef.current ) { + return; } + + this.listRef.current.scrollToTop(); }; renderLoadingPlaceholders = () => { @@ -726,8 +741,24 @@ class ReaderStream extends Component { } } +/** + * Returns a modified stream key if necessary else returns the original stream key. + * @returns {string} Stream key. + */ +function getStreamKey( state, streamKey ) { + // For "following" stream, use a unique streamKey if a feed is selected. This prevent feed overwrites when rapid selections are made. + const selectedFeedId = getSelectedFeedId( state ); + const isFollowingFiltered = streamKey === 'following' && selectedFeedId; + if ( isFollowingFiltered ) { + return `following:feed-${ selectedFeedId }`; + } + + return streamKey; +} + export default connect( - ( state, { streamKey, recsStreamKey } ) => { + ( state, { streamKey: tempStreamKey, recsStreamKey } ) => { + const streamKey = getStreamKey( state, tempStreamKey ); const stream = getStream( state, streamKey ); const selectedPost = getPostByKey( state, stream.selected ); @@ -744,7 +775,9 @@ export default connect( } ), notificationsOpen: isNotificationsOpen( state ), stream, + streamKey, recsStream: getStream( state, recsStreamKey ), + selectedFeedId: getSelectedFeedId( state ), selectedPostKey: stream.selected, selectedPost, lastPage: stream.lastPage, @@ -757,6 +790,7 @@ export default connect( }; }, { + clearStream, resetCardExpansions, likePost, unlikePost, diff --git a/client/state/data-layer/wpcom/read/streams/index.js b/client/state/data-layer/wpcom/read/streams/index.js index 627ddd6c4f848..82e505f760519 100644 --- a/client/state/data-layer/wpcom/read/streams/index.js +++ b/client/state/data-layer/wpcom/read/streams/index.js @@ -352,7 +352,7 @@ const streamApis = { */ export function requestPage( action ) { const { - payload: { streamKey, streamType, pageHandle, isPoll, gap, localeSlug, page, perPage }, + payload: { streamKey, streamType, feedId, pageHandle, isPoll, gap, localeSlug, page, perPage }, } = action; const api = streamApis[ streamType ]; @@ -385,6 +385,7 @@ export function requestPage( action ) { // There is a race condition in switchLocale when retrieving the language file // The stream request can occur before the language file is loaded, so we need a way to explicitly set the lang in the request const lang = localeSlug || i18n.getLocaleSlug(); + const commonQueryParams = { ...algorithm, feed_id: feedId }; return http( { method: 'GET', @@ -392,8 +393,8 @@ export function requestPage( action ) { apiVersion, apiNamespace: api.apiNamespace ?? null, query: isPoll - ? pollQuery( [], { ...algorithm } ) - : query( { ...pageHandle, ...algorithm, number, lang, page }, action.payload ), + ? pollQuery( [], commonQueryParams ) + : query( { ...commonQueryParams, ...pageHandle, number, lang, page }, action.payload ), onSuccess: action, onFailure: action, } ); diff --git a/client/state/data-layer/wpcom/read/streams/test/index.js b/client/state/data-layer/wpcom/read/streams/test/index.js index 209777dac1eff..7915b20a2bcb8 100644 --- a/client/state/data-layer/wpcom/read/streams/test/index.js +++ b/client/state/data-layer/wpcom/read/streams/test/index.js @@ -42,7 +42,7 @@ describe( 'streams', () => { it( 'should set proper params for subsequent fetches', () => { const pageHandle = { after: 'the robots attack' }; - const secondPage = { ...action, payload: { ...action.payload, pageHandle } }; + const secondPage = { ...action, payload: { ...action.payload, pageHandle, feedId: 1234 } }; const httpAction = requestPage( secondPage ); expect( httpAction ).toEqual( @@ -50,7 +50,7 @@ describe( 'streams', () => { method: 'GET', path: '/read/following', apiVersion: '1.2', - query: { ...query, number: PER_FETCH, after: 'the robots attack' }, + query: { ...query, feed_id: 1234, number: PER_FETCH, after: 'the robots attack' }, onSuccess: secondPage, onFailure: secondPage, } ) diff --git a/client/state/reader-ui/sidebar/selectors.js b/client/state/reader-ui/sidebar/selectors.js deleted file mode 100644 index ea1805879d68c..0000000000000 --- a/client/state/reader-ui/sidebar/selectors.js +++ /dev/null @@ -1,42 +0,0 @@ -import 'calypso/state/reader-ui/init'; - -/** - * Whether or not a specific reader organization sidebar item is open - * @param state redux state - * @param organizationId given org id - * @returns {boolean} whether or not the sidebar item is open - */ -export function isOrganizationOpen( state, organizationId ) { - const openOrganizations = state.readerUi.sidebar.openOrganizations; - if ( openOrganizations.indexOf( organizationId ) > -1 ) { - return true; - } - return false; -} - -/** - * Whether or not following reader sidebar item is open - * @param state redux state - * @returns {boolean} whether or not the sidebar item is open - */ -export function isFollowingOpen( state ) { - return state.readerUi.sidebar.isFollowingOpen; -} - -/** - * Whether or not lists reader sidebar item is open - * @param state redux state - * @returns {boolean} whether or not the sidebar item is open - */ -export function isListsOpen( state ) { - return state.readerUi.sidebar.isListsOpen; -} - -/** - * Whether or not tags reader sidebar item is open - * @param state redux state - * @returns {boolean} whether or not the sidebar item is open - */ -export function isTagsOpen( state ) { - return state.readerUi.sidebar.isTagsOpen; -} diff --git a/client/state/reader-ui/sidebar/selectors.ts b/client/state/reader-ui/sidebar/selectors.ts new file mode 100644 index 0000000000000..7a3e2662ccdff --- /dev/null +++ b/client/state/reader-ui/sidebar/selectors.ts @@ -0,0 +1,41 @@ +import 'calypso/state/reader-ui/init'; +import { AppState } from 'calypso/types'; + +/** + * Whether or not a specific reader organization sidebar item is open. + */ +export function isOrganizationOpen( state: AppState, organizationId: number ): boolean { + const openOrganizations = state.readerUi.sidebar.openOrganizations; + if ( openOrganizations.indexOf( organizationId ) > -1 ) { + return true; + } + return false; +} + +/** + * Whether or not following reader sidebar item is open. + */ +export function isFollowingOpen( state: AppState ): boolean { + return state.readerUi.sidebar.isFollowingOpen; +} + +/** + * Whether or not lists reader sidebar item is open. + */ +export function isListsOpen( state: AppState ): boolean { + return state.readerUi.sidebar.isListsOpen; +} + +/** + * Whether or not tags reader sidebar item is open. + */ +export function isTagsOpen( state: AppState ): boolean { + return state.readerUi.sidebar.isTagsOpen; +} + +/** + * Get the selected feed ID from the reader sidebar. + */ +export function getSelectedFeedId( state: AppState ): number | null { + return state.readerUi.sidebar.selectedRecentSite; +} diff --git a/client/state/reader/analytics/actions.js b/client/state/reader/analytics/actions.js index 6fd75908c3208..c7ed9b9f6da97 100644 --- a/client/state/reader/analytics/actions.js +++ b/client/state/reader/analytics/actions.js @@ -1,16 +1,19 @@ -import { buildReaderTracksEventProps } from 'calypso/reader/stats'; -import { recordTracksEvent } from 'calypso/state/analytics/actions'; import { getReaderFollowsCount } from 'calypso/state/reader/follows/selectors'; +import { dispatchReaderTracksEvent } from './analytics.utils'; +/** + * Record a tracks event with additional reader-specific properties. + * @returns The action object to dispatch. + */ export const recordReaderTracksEvent = ( name, properties, { pathnameOverride, post } = {} ) => ( dispatch, getState ) => { - const eventProps = buildReaderTracksEventProps( properties, pathnameOverride, post ); const followsCount = getReaderFollowsCount( getState() ); - dispatch( - recordTracksEvent( name, { - subscription_count: followsCount, - ...eventProps, - } ) + + dispatchReaderTracksEvent( + dispatch, + name, + { ...properties, subscription_count: followsCount }, + { pathnameOverride, post } ); }; diff --git a/client/state/reader/analytics/analytics.utils.ts b/client/state/reader/analytics/analytics.utils.ts new file mode 100644 index 0000000000000..7b23cf255b462 --- /dev/null +++ b/client/state/reader/analytics/analytics.utils.ts @@ -0,0 +1,27 @@ +import { buildReaderTracksEventProps } from 'calypso/reader/stats'; +import { recordEvent, recordTracksEvent } from 'calypso/state/analytics/actions'; + +export interface ReaderTrackEventProps { + [ key: string ]: string | number | undefined; + subscription_count?: number; +} + +export interface ReaderTrackEventOptions { + pathnameOverride?: string; + post: object | null; +} + +/** + * A utility function to dispatch a reader specific Tracks event. + * + * Note: This function shouldn't be used directly, either use `useRecordReaderTracksEvent` hook or `recordReaderTracksEvent` Redux action. + */ +export function dispatchReaderTracksEvent( + dispatch: ( action: ReturnType< typeof recordEvent > ) => void, + eventName: string, + properties: ReaderTrackEventProps = {}, + { pathnameOverride, post }: ReaderTrackEventOptions = { post: null } +): void { + const eventProps = buildReaderTracksEventProps( properties, pathnameOverride, post ); + dispatch( recordTracksEvent( eventName, eventProps ) ); +} diff --git a/client/state/reader/analytics/useRecordReaderTracksEvent.ts b/client/state/reader/analytics/useRecordReaderTracksEvent.ts new file mode 100644 index 0000000000000..4a8ef5a54916b --- /dev/null +++ b/client/state/reader/analytics/useRecordReaderTracksEvent.ts @@ -0,0 +1,28 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { getReaderFollowsCount } from '../follows/selectors'; +import { + ReaderTrackEventOptions, + ReaderTrackEventProps, + dispatchReaderTracksEvent, +} from './analytics.utils'; + +/** + * A hook version of recordReaderTracksEvent action creator. + */ +export const useRecordReaderTracksEvent = () => { + const dispatch = useDispatch(); + const followsCount = useSelector( getReaderFollowsCount ); + + return ( + name: string, + properties: ReaderTrackEventProps = {}, + { pathnameOverride, post }: ReaderTrackEventOptions = { post: null } + ): void => { + return dispatchReaderTracksEvent( + dispatch, + name, + { ...properties, subscription_count: followsCount }, + { pathnameOverride, post } + ); + }; +}; diff --git a/client/state/reader/streams/actions.js b/client/state/reader/streams/actions.js index b298368fd6a9d..e52e4d22d50ec 100644 --- a/client/state/reader/streams/actions.js +++ b/client/state/reader/streams/actions.js @@ -11,9 +11,7 @@ import { READER_STREAMS_CLEAR, } from 'calypso/state/reader/action-types'; import { getStream } from 'calypso/state/reader/streams/selectors'; - import 'calypso/state/data-layer/wpcom/read/streams'; - import 'calypso/state/reader/init'; /** @@ -27,6 +25,7 @@ import 'calypso/state/reader/init'; */ export function requestPage( { streamKey, + feedId, pageHandle, isPoll = false, gap = null, @@ -43,6 +42,7 @@ export function requestPage( { isPoll, gap, localeSlug, + feedId, }, }; }