From 0fce49bf06884db799bbf61629448e1bf611ea00 Mon Sep 17 00:00:00 2001 From: Allison Levine <1689238+allilevine@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:00:53 -0500 Subject: [PATCH] Reader Recent Feed: Refetch subscriptions from empty state (#97030) * Extract subscription logic and refresh subscriptions when the count changes. * Clean up hook for readability. * Rename hook, simplify return, and add JSDoc comment. * Rename file and add tests. --- client/reader/following/main.tsx | 27 +--- .../test/use-site-subscriptions.test.ts | 120 ++++++++++++++++++ .../following/use-site-subscriptions.ts | 50 ++++++++ 3 files changed, 172 insertions(+), 25 deletions(-) create mode 100644 client/reader/following/test/use-site-subscriptions.test.ts create mode 100644 client/reader/following/use-site-subscriptions.ts diff --git a/client/reader/following/main.tsx b/client/reader/following/main.tsx index e7e961ac58983a..f6600f88fddd36 100644 --- a/client/reader/following/main.tsx +++ b/client/reader/following/main.tsx @@ -1,8 +1,6 @@ import config from '@automattic/calypso-config'; -import { SubscriptionManager } from '@automattic/data-stores'; import clsx from 'clsx'; import { translate } from 'i18n-calypso'; -import { useMemo } from 'react'; import AsyncLoad from 'calypso/components/async-load'; import BloganuaryHeader from 'calypso/components/bloganuary-header'; import NavigationHeader from 'calypso/components/navigation-header'; @@ -12,35 +10,14 @@ import SuggestionProvider from 'calypso/reader/search-stream/suggestion-provider import ReaderStream, { WIDE_DISPLAY_CUTOFF } from 'calypso/reader/stream'; import Recent from '../recent'; import ReaderStreamSidebar from './reader-stream-sidebar'; +import { useSiteSubscriptions } from './use-site-subscriptions'; import { useFollowingView } from './view-preference'; import ViewToggle from './view-toggle'; import './style.scss'; function FollowingStream( { ...props } ) { const { currentView } = useFollowingView(); - const { data: subscriptionsCount, isLoading: isLoadingCount } = - SubscriptionManager.useSubscriptionsCountQuery(); - const { data: siteSubscriptions, isLoading: isLoadingSiteSubscriptions } = - SubscriptionManager.useSiteSubscriptionsQuery(); - - const isLoading = isLoadingCount || isLoadingSiteSubscriptions; - - const hasNonSelfSubscriptions = useMemo( () => { - if ( ! subscriptionsCount?.blogs || subscriptionsCount?.blogs === 0 ) { - return false; - } - - // If we have site subscriptions data, filter out self-owned blogs. - if ( siteSubscriptions?.subscriptions ) { - const nonSelfSubscriptions = siteSubscriptions.subscriptions.filter( - ( sub ) => ! sub.is_owner - ); - return nonSelfSubscriptions.length > 0; - } - - return subscriptionsCount.blogs > 0; - }, [ subscriptionsCount, siteSubscriptions ] ); - + const { isLoading, hasNonSelfSubscriptions } = useSiteSubscriptions(); const viewToggle = config.isEnabled( 'reader/recent-feed-overhaul' ) ? : null; if ( ! isLoading && ! hasNonSelfSubscriptions ) { diff --git a/client/reader/following/test/use-site-subscriptions.test.ts b/client/reader/following/test/use-site-subscriptions.test.ts new file mode 100644 index 00000000000000..c61dfdffe1c5ee --- /dev/null +++ b/client/reader/following/test/use-site-subscriptions.test.ts @@ -0,0 +1,120 @@ +/** + * @jest-environment jsdom + */ +import { SubscriptionManager } from '@automattic/data-stores'; +import { renderHook } from '@testing-library/react'; +import { useSiteSubscriptions } from '../use-site-subscriptions'; + +jest.mock( '@automattic/data-stores', () => ( { + SubscriptionManager: { + useSubscriptionsCountQuery: jest.fn(), + useSiteSubscriptionsQuery: jest.fn(), + }, +} ) ); + +describe( 'useSiteSubscriptions', () => { + beforeEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should return loading state when either query is loading', () => { + ( SubscriptionManager.useSubscriptionsCountQuery as jest.Mock ).mockReturnValue( { + data: null, + isLoading: true, + } ); + ( SubscriptionManager.useSiteSubscriptionsQuery as jest.Mock ).mockReturnValue( { + data: null, + isLoading: false, + refetch: jest.fn(), + } ); + + const { result } = renderHook( () => useSiteSubscriptions() ); + + expect( result.current.isLoading ).toBe( true ); + } ); + + it( 'should return false for hasNonSelfSubscriptions when blog count is 0', () => { + ( SubscriptionManager.useSubscriptionsCountQuery as jest.Mock ).mockReturnValue( { + data: { blogs: 0 }, + isLoading: false, + } ); + ( SubscriptionManager.useSiteSubscriptionsQuery as jest.Mock ).mockReturnValue( { + data: null, + isLoading: false, + refetch: jest.fn(), + } ); + + const { result } = renderHook( () => useSiteSubscriptions() ); + + expect( result.current.hasNonSelfSubscriptions ).toBe( false ); + } ); + + it( 'should filter out self-owned blogs when calculating hasNonSelfSubscriptions', () => { + ( SubscriptionManager.useSubscriptionsCountQuery as jest.Mock ).mockReturnValue( { + data: { blogs: 2 }, + isLoading: false, + } ); + ( SubscriptionManager.useSiteSubscriptionsQuery as jest.Mock ).mockReturnValue( { + data: { + subscriptions: [ { is_owner: true }, { is_owner: false } ], + }, + isLoading: false, + refetch: jest.fn(), + } ); + + const { result } = renderHook( () => useSiteSubscriptions() ); + + expect( result.current.hasNonSelfSubscriptions ).toBe( true ); + } ); + + it( 'should return false for hasNonSelfSubscriptions when all subscriptions are self-owned', () => { + ( SubscriptionManager.useSubscriptionsCountQuery as jest.Mock ).mockReturnValue( { + data: { blogs: 2 }, + isLoading: false, + } ); + ( SubscriptionManager.useSiteSubscriptionsQuery as jest.Mock ).mockReturnValue( { + data: { + subscriptions: [ { is_owner: true }, { is_owner: true } ], + }, + isLoading: false, + refetch: jest.fn(), + } ); + + const { result } = renderHook( () => useSiteSubscriptions() ); + + expect( result.current.hasNonSelfSubscriptions ).toBe( false ); + } ); + + it( 'should return true for hasNonSelfSubscriptions when blog count > 0 but no subscription data yet', () => { + ( SubscriptionManager.useSubscriptionsCountQuery as jest.Mock ).mockReturnValue( { + data: { blogs: 1 }, + isLoading: false, + } ); + ( SubscriptionManager.useSiteSubscriptionsQuery as jest.Mock ).mockReturnValue( { + data: null, + isLoading: false, + refetch: jest.fn(), + } ); + + const { result } = renderHook( () => useSiteSubscriptions() ); + + expect( result.current.hasNonSelfSubscriptions ).toBe( true ); + } ); + + it( 'should call refetch when blog count changes from 0 to positive', () => { + const refetchMock = jest.fn(); + ( SubscriptionManager.useSubscriptionsCountQuery as jest.Mock ).mockReturnValue( { + data: { blogs: 1 }, + isLoading: false, + } ); + ( SubscriptionManager.useSiteSubscriptionsQuery as jest.Mock ).mockReturnValue( { + data: null, + isLoading: false, + refetch: refetchMock, + } ); + + renderHook( () => useSiteSubscriptions() ); + + expect( refetchMock ).toHaveBeenCalled(); + } ); +} ); diff --git a/client/reader/following/use-site-subscriptions.ts b/client/reader/following/use-site-subscriptions.ts new file mode 100644 index 00000000000000..309218642f25c0 --- /dev/null +++ b/client/reader/following/use-site-subscriptions.ts @@ -0,0 +1,50 @@ +import { SubscriptionManager } from '@automattic/data-stores'; +import { useMemo, useEffect } from 'react'; + +/** + * Custom hook to manage site subscriptions data. + * Fetches and tracks subscription counts and site subscription details, + * filtering out self-owned blogs to determine if the user has any external subscriptions. + * @returns {Object} An object containing: + * - isLoading: boolean indicating if subscription data is being loaded + * - hasNonSelfSubscriptions: boolean indicating if user has any subscriptions to non-self-owned blogs + */ +export function useSiteSubscriptions() { + const { data: subscriptionsCount, isLoading: isLoadingCount } = + SubscriptionManager.useSubscriptionsCountQuery(); + const { + data: siteSubscriptions, + isLoading: isLoadingSiteSubscriptions, + refetch: refetchSiteSubscriptions, + } = SubscriptionManager.useSiteSubscriptionsQuery(); + + const isLoading = isLoadingCount || isLoadingSiteSubscriptions; + const blogCount = subscriptionsCount?.blogs ?? 0; + + const hasNonSelfSubscriptions = useMemo( () => { + if ( blogCount === 0 ) { + return false; + } + + // If we have site subscriptions data, filter out self-owned blogs. + if ( siteSubscriptions?.subscriptions ) { + const nonSelfSubscriptions = siteSubscriptions.subscriptions.filter( + ( sub ) => ! sub.is_owner + ); + return nonSelfSubscriptions.length > 0; + } + + return true; + }, [ blogCount, siteSubscriptions ] ); + + useEffect( () => { + if ( blogCount > 0 ) { + refetchSiteSubscriptions(); + } + }, [ refetchSiteSubscriptions, blogCount ] ); + + return { + isLoading, + hasNonSelfSubscriptions, + }; +}