diff --git a/packages/shared/src/components/CustomFeedEmptyScreen.tsx b/packages/shared/src/components/CustomFeedEmptyScreen.tsx index 02f57a81e4..5bdc427391 100644 --- a/packages/shared/src/components/CustomFeedEmptyScreen.tsx +++ b/packages/shared/src/components/CustomFeedEmptyScreen.tsx @@ -1,53 +1,114 @@ import type { ReactElement } from 'react'; import React from 'react'; -import { useRouter } from 'next/router'; -import { - EmptyScreenButton, - EmptyScreenContainer, - EmptyScreenDescription, - EmptyScreenIcon, - EmptyScreenTitle, -} from './EmptyScreen'; -import { HashtagIcon } from './icons'; -import { PageContainer } from './utilities'; -import { ButtonSize } from './buttons/common'; +import { EmptyScreenIcon } from './EmptyScreen'; +import { DevPlusIcon, HashtagIcon } from './icons'; +import { PageContainer, SharedFeedPage } from './utilities'; +import { ButtonSize, ButtonVariant } from './buttons/common'; import { webappUrl } from '../lib/constants'; +import { + DEFAULT_ALGORITHM_INDEX, + DEFAULT_ALGORITHM_KEY, + SearchControlHeader, +} from './layout/common'; +import usePersistentContext from '../hooks/usePersistentContext'; +import { + Typography, + TypographyColor, + TypographyTag, + TypographyType, +} from './typography/Typography'; +import { LogEvent, TargetId } from '../lib/log'; +import { Button } from './buttons/Button'; import { usePlusSubscription } from '../hooks'; -import { DeleteCustomFeed } from './buttons/DeleteCustomFeed'; +import { IconSize } from './Icon'; export const CustomFeedEmptyScreen = (): ReactElement => { - const { isPlus } = usePlusSubscription(); - const router = useRouter(); - + const { logSubscriptionEvent, showPlusSubscription, isPlus } = + usePlusSubscription(); + const [selectedAlgo, setSelectedAlgo] = usePersistentContext( + DEFAULT_ALGORITHM_KEY, + DEFAULT_ALGORITHM_INDEX, + [0, 1], + DEFAULT_ALGORITHM_INDEX, + ); return ( - - - +
+ - Let's set up your feed! - - Start by configuring your feed settings to tailor content to your - interests. Add tags, filters, and sources to make it truly yours. - -
- { - router.push(`${webappUrl}feeds/${router.query.slugOrId}/edit`); - }} - size={ButtonSize.Large} - > - Set up feed - - {!isPlus ? ( - - ) : null} +
+ +
+ + {showPlusSubscription && !isPlus ? ( + <> + + Plus + + + Custom feeds got a massive upgrade! + + + Custom Feeds is now more powerful than ever before, with + advanced filters, extensive customization options, and complete + feed control. Upgrade to Plus to unlock this ultimate tool for + tailoring your content. + + + + ) : ( + <> + + Your feed filters are too specific. + + + We couldn't fetch enough posts based on your selected tags. + Try adding more tags using the feed settings. + + + )}
- -
+ +
); }; diff --git a/packages/shared/src/components/CustomFeedOptionsMenu.tsx b/packages/shared/src/components/CustomFeedOptionsMenu.tsx index 3996b25761..1985b260f8 100644 --- a/packages/shared/src/components/CustomFeedOptionsMenu.tsx +++ b/packages/shared/src/components/CustomFeedOptionsMenu.tsx @@ -29,7 +29,7 @@ const CustomFeedOptionsMenu = ({ onUndo, onCreateNewFeed, }: CustomFeedOptionsMenuProps): ReactElement => { - const { showPlusSubscription, isPlus } = usePlusSubscription(); + const { showPlusSubscription } = usePlusSubscription(); const { openModal } = useLazyModal(); const [, onShareOrCopyLink] = useShareOrCopyLink(shareProps); const { isOpen, onMenuClick } = useContextMenu({ @@ -38,11 +38,6 @@ const CustomFeedOptionsMenu = ({ const { feeds } = useFeeds(); const handleOpenModal = () => { - if (!isPlus) { - return openModal({ - type: LazyModal.AdvancedCustomFeedSoon, - }); - } if (feeds?.edges?.length > 0) { return openModal({ type: LazyModal.AddToCustomFeed, diff --git a/packages/shared/src/components/feeds/FeedNav.tsx b/packages/shared/src/components/feeds/FeedNav.tsx index ffb7f71563..8f81a90c47 100644 --- a/packages/shared/src/components/feeds/FeedNav.tsx +++ b/packages/shared/src/components/feeds/FeedNav.tsx @@ -6,12 +6,7 @@ import dynamic from 'next/dynamic'; import { Tab, TabContainer } from '../tabs/TabContainer'; import { useActiveFeedNameContext } from '../../contexts'; import useActiveNav from '../../hooks/useActiveNav'; -import { - useFeeds, - usePlusSubscription, - useViewSize, - ViewSize, -} from '../../hooks'; +import { useFeeds, useViewSize, ViewSize } from '../../hooks'; import usePersistentContext from '../../hooks/usePersistentContext'; import { algorithmsList, @@ -26,14 +21,12 @@ import { PlusIcon, SortIcon } from '../icons'; import { ButtonSize, ButtonVariant } from '../buttons/common'; import { useScrollTopClassName } from '../../hooks/useScrollTopClassName'; import { useFeatureTheme } from '../../hooks/utils/useFeatureTheme'; -import { customFeedsPlusDate, webappUrl } from '../../lib/constants'; +import { webappUrl } from '../../lib/constants'; import NotificationsBell from '../notifications/NotificationsBell'; import classed from '../../lib/classed'; import { useAuthContext } from '../../contexts/AuthContext'; import { OtherFeedPage } from '../../lib/query'; import { ChecklistViewState } from '../../lib/checklist'; -import { LazyModal } from '../modals/common/types'; -import { useLazyModal } from '../../hooks/useLazyModal'; import useCustomDefaultFeed from '../../hooks/feed/useCustomDefaultFeed'; const OnboardingChecklistBar = dynamic( @@ -80,8 +73,6 @@ function FeedNav(): ReactElement { const scrollClassName = useScrollTopClassName({ enabled: !!featureTheme }); const { feeds } = useFeeds(); const { isCustomDefaultFeed, defaultFeedId } = useCustomDefaultFeed(); - const { showPlusSubscription, isPlus } = usePlusSubscription(); - const { openModal } = useLazyModal(); const isHiddenOnboardingChecklistView = onboardingChecklistView === ChecklistViewState.Hidden; @@ -180,41 +171,6 @@ function FeedNav(): ReactElement { return null; }} - onActiveChange={(label, event) => { - if ( - showPlusSubscription && - label === FeedNavTab.NewFeed && - !isPlus - ) { - event.preventDefault(); - - openModal({ type: LazyModal.AdvancedCustomFeedSoon, props: {} }); - - return false; - } - - const feedNavItem = feeds?.edges?.find( - ({ node }) => node.flags.name === label, - ); - - if ( - showPlusSubscription && - !isPlus && - feedNavItem && - new Date(feedNavItem.node.createdAt) > customFeedsPlusDate - ) { - event.preventDefault(); - - openModal({ - type: LazyModal.AdvancedCustomFeedSoon, - props: {}, - }); - - return false; - } - - return true; - }} > {Object.entries(urlToTab).map(([url, label]) => ( diff --git a/packages/shared/src/components/feeds/FeedSettings/FeedSettingsCreate.tsx b/packages/shared/src/components/feeds/FeedSettings/FeedSettingsCreate.tsx index e7a50c2438..d7eb3d28fc 100644 --- a/packages/shared/src/components/feeds/FeedSettings/FeedSettingsCreate.tsx +++ b/packages/shared/src/components/feeds/FeedSettings/FeedSettingsCreate.tsx @@ -44,12 +44,12 @@ export const FeedSettingsCreate = (): ReactElement => { const queryClient = useQueryClient(); const { displayToast } = useToastNotification(); const { logEvent } = useLogContext(); + const { showPlusSubscription } = usePlusSubscription(); const [data, setData] = useState(() => ({ icon: '', })); const { createFeed } = useFeeds(); const { follow } = useContentPreference({ showToastOnSuccess: false }); - const { isPlus } = usePlusSubscription(); const { onFinished, delayedRedirect, isAnimating } = useProgressAnimation({ animationMs: 1000, @@ -140,16 +140,6 @@ export const FeedSettingsCreate = (): ReactElement => { router.replace(webappUrl); }; - useEffect(() => { - if (!isPlus) { - router.replace(webappUrl); - } - }, [isPlus, router]); - - if (!isPlus) { - return null; - } - return ( { > New custom feed - + {showPlusSubscription && }
-
- ), - }, + ]; + + if (showPlusSubscription || feed?.type === FeedType.Main) { + base.push( + { + title: feedSettingsMenuTitle.tags, + options: { icon: }, }, - { - title: feedSettingsMenuTitle.sources, - options: { icon: }, - }, - { - title: feedSettingsMenuTitle.preferences, - options: { icon: }, - }, - { - title: feedSettingsMenuTitle.ai, - options: { icon: }, - }, - feed?.type === FeedType.Custom && { - title: feedSettingsMenuTitle.filters, - options: { icon: }, - }, - { - title: feedSettingsMenuTitle.blocking, - options: { icon: }, - }, - ].filter(Boolean); - }, [feed?.type, isPlus, showPlusSubscription, logSubscriptionEvent]); + { + title: feedSettingsMenuTitle.sources, + options: { icon: }, + }, + { + title: feedSettingsMenuTitle.preferences, + options: { icon: }, + }, + { + title: feedSettingsMenuTitle.ai, + options: { icon: }, + }, + feed?.type === FeedType.Custom && { + title: feedSettingsMenuTitle.filters, + options: { icon: }, + }, + { + title: feedSettingsMenuTitle.blocking, + options: { icon: }, + }, + ); + } + + return base.filter(Boolean); + }, [feed?.type, showPlusSubscription]); const defaultView = useMemo(() => { return feedSettingsMenuTitle[router.query.dview as FeedSettingsMenu]; }, [router.query.dview]); - const canEditFeed = isPlus || feed?.type === FeedType.Main; - - useEffect(() => { - if (!canEditFeed) { - router.replace(webappUrl); - } - }, [canEditFeed, router, feedSlugOrId]); - - if (!canEditFeed) { - return null; - } - if (!feed) { return null; } diff --git a/packages/shared/src/components/feeds/FeedSettings/FeedSettingsEditHeader.tsx b/packages/shared/src/components/feeds/FeedSettings/FeedSettingsEditHeader.tsx index 7104b5dfbc..6d63e400cc 100644 --- a/packages/shared/src/components/feeds/FeedSettings/FeedSettingsEditHeader.tsx +++ b/packages/shared/src/components/feeds/FeedSettings/FeedSettingsEditHeader.tsx @@ -7,22 +7,17 @@ import { ButtonSize, ButtonVariant } from '../../buttons/common'; import { Modal } from '../../modals/common/Modal'; import { ModalPropsContext } from '../../modals/common/types'; import { FeedSettingsTitle } from './FeedSettingsTitle'; -import { feedSettingsMenuTitle } from './types'; - -// for now only some views have save button -// on other views settings are auto saved -const viewsWithSaveButton = new Set([ - feedSettingsMenuTitle.general, - feedSettingsMenuTitle.filters, -]); +import { usePlusSubscription } from '../../../hooks'; +import { webappUrl } from '../../../lib/constants'; +import { DevPlusIcon } from '../../icons'; +import { LogEvent, TargetId } from '../../../lib/log'; export const FeedSettingsEditHeader = (): ReactElement => { const { onSubmit, onDiscard, isSubmitPending, isDirty, onBackToFeed } = useContext(FeedSettingsEditContext); const { activeView, setActiveView } = useContext(ModalPropsContext); const isMobile = useViewSizeClient(ViewSize.MobileL); - - const viewHasSaveButton = viewsWithSaveButton.has(activeView); + const { isEnrolledNotPlus, logSubscriptionEvent } = usePlusSubscription(); if (!activeView) { return null; @@ -32,31 +27,48 @@ export const FeedSettingsEditHeader = (): ReactElement => { - {viewHasSaveButton && ( -
+
+ + {isEnrolledNotPlus ? ( + ) : ( -
- )} + )} +
); }; diff --git a/packages/shared/src/components/feeds/FeedSettings/sections/FeedSettingsBlockingSection.tsx b/packages/shared/src/components/feeds/FeedSettings/sections/FeedSettingsBlockingSection.tsx index 98d1966635..039adff9e9 100644 --- a/packages/shared/src/components/feeds/FeedSettings/sections/FeedSettingsBlockingSection.tsx +++ b/packages/shared/src/components/feeds/FeedSettings/sections/FeedSettingsBlockingSection.tsx @@ -19,6 +19,7 @@ import { BlockedSourceList } from '../components/BlockedSourceList'; import { SourceType } from '../../../../graphql/sources'; import { BlockedUserList } from '../components/BlockedUserList'; import { BlockedTagList } from '../components/BlockedTagList'; +import { usePlusSubscription } from '../../../../hooks'; enum FeedSettingsBlockingSectionTabs { Sources = 'Sources', @@ -32,6 +33,7 @@ const tabs = Object.values(FeedSettingsBlockingSectionTabs); const noop = () => undefined; export const FeedSettingsBlockingSection = (): ReactElement => { + const { showPlusSubscription } = usePlusSubscription(); const [activeView, setActiveView] = useState( () => FeedSettingsBlockingSectionTabs.Sources, ); @@ -55,7 +57,7 @@ export const FeedSettingsBlockingSection = (): ReactElement => { Manage everything you’ve excluded from your feed. Search and block sources, squads, users, or tags to fine-tune your content. - + {showPlusSubscription ? : undefined} { const { updateUserProfile } = useProfileForm(); const isMainFeed = feed?.type === FeedType.Main; const isCustomFeed = feed?.type === FeedType.Custom; - const { isPlus } = usePlusSubscription(); + const { isPlus, showPlusSubscription } = usePlusSubscription(); const isDefaultFeed = isMainFeed ? user.defaultFeedId === null @@ -97,7 +97,7 @@ export const FeedSettingsGeneralSection = (): ReactElement => { /> )} - {isCustomFeed && ( + {isCustomFeed && showPlusSubscription && (
Choose an icon @@ -133,69 +133,73 @@ export const FeedSettingsGeneralSection = (): ReactElement => {
)} -
-
- - Set as your default feed - - - Make this feed the first one you see every time you open daily.dev. - + {showPlusSubscription && ( +
+
+ + Set as your default feed + + + Make this feed the first one you see every time you open + daily.dev. + +
+ {isCustomFeed && ( + + )} + {isMainFeed && ( + +
+ +
+
+ )}
- {isCustomFeed && ( - - )} - {isMainFeed && ( - -
- -
-
- )} -
+ )} {isCustomFeed && ( <> diff --git a/packages/shared/src/components/feeds/MobileFeedActions.tsx b/packages/shared/src/components/feeds/MobileFeedActions.tsx index 5bfd28ee6b..401849c5df 100644 --- a/packages/shared/src/components/feeds/MobileFeedActions.tsx +++ b/packages/shared/src/components/feeds/MobileFeedActions.tsx @@ -14,17 +14,12 @@ import { LogoPosition } from '../Logo'; import { webappUrl } from '../../lib/constants'; import useCustomDefaultFeed from '../../hooks/feed/useCustomDefaultFeed'; import { getFeedName } from '../../lib/feed'; -import { usePlusSubscription } from '../../hooks'; -import { useLazyModal } from '../../hooks/useLazyModal'; -import { LazyModal } from '../modals/common/types'; export function MobileFeedActions(): ReactElement { const router = useRouter(); const { user } = useAuthContext(); const { streak, isLoading, isStreaksEnabled } = useReadingStreak(); const { isCustomDefaultFeed, defaultFeedId } = useCustomDefaultFeed(); - const { isEnrolledNotPlus } = usePlusSubscription(); - const { openModal } = useLazyModal(); return (
@@ -44,15 +39,6 @@ export function MobileFeedActions(): ReactElement { { - if (isEnrolledNotPlus) { - openModal({ - type: LazyModal.AdvancedCustomFeedSoon, - props: {}, - }); - - return; - } - if (isCustomDefaultFeed && router.pathname === '/') { router.push(`${webappUrl}feeds/${defaultFeedId}/edit`); } else { diff --git a/packages/shared/src/components/filters/AdvancedSettings.spec.tsx b/packages/shared/src/components/filters/AdvancedSettings.spec.tsx deleted file mode 100644 index 19f980527e..0000000000 --- a/packages/shared/src/components/filters/AdvancedSettings.spec.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import nock from 'nock'; -import React from 'react'; -import type { RenderResult } from '@testing-library/react'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import defaultUser from '../../../__tests__/fixture/loggedUser'; -import AuthContext from '../../contexts/AuthContext'; -import type { MockedGraphQLResponse } from '../../../__tests__/helpers/graphql'; -import { mockGraphQL } from '../../../__tests__/helpers/graphql'; -import type { LoggedUser } from '../../lib/user'; -import type { - AdvancedSettings, - AllTagCategoriesData, - FeedSettings, -} from '../../graphql/feedSettings'; -import { - AdvancedSettingsGroup, - FEED_SETTINGS_QUERY, - UPDATE_ADVANCED_SETTINGS_FILTERS_MUTATION, -} from '../../graphql/feedSettings'; -import AdvancedSettingsPage from './AdvancedSettings'; -import { getFeedSettingsQueryKey } from '../../hooks/useFeedSettings'; -import { waitForNock } from '../../../__tests__/helpers/utilities'; -import type { AlertContextData } from '../../contexts/AlertContext'; -import AlertContext, { ALERT_DEFAULTS } from '../../contexts/AlertContext'; - -const showLogin = jest.fn(); -let loggedUser: LoggedUser; - -beforeEach(() => { - loggedUser = undefined; - jest.restoreAllMocks(); - jest.restoreAllMocks(); - jest.clearAllMocks(); - nock.cleanAll(); -}); - -const createAdvancedSettingsAndFiltersMock = ( - feedSettings: FeedSettings = { - advancedSettings: [{ id: 1, enabled: false }], - }, - advancedSettings: AdvancedSettings[] = [ - { - id: 1, - title: 'Tech magazines', - description: 'Description for Tech magazines', - defaultEnabledState: true, - group: AdvancedSettingsGroup.ContentCuration, - }, - { - id: 2, - title: 'Newsletters', - description: 'Description for Newsletters', - defaultEnabledState: true, - group: AdvancedSettingsGroup.ContentCuration, - }, - ], -): MockedGraphQLResponse => ({ - request: { query: FEED_SETTINGS_QUERY }, - result: { - data: { - feedSettings, - advancedSettings, - }, - }, -}); - -let client: QueryClient; - -const renderComponent = ( - mocks: MockedGraphQLResponse[] = [createAdvancedSettingsAndFiltersMock()], - alertsData: AlertContextData = { alerts: ALERT_DEFAULTS }, -): RenderResult => { - client = new QueryClient(); - mocks.forEach(mockGraphQL); - return render( - - - - - - - , - ); -}; - -it('should display advanced settings title and description', async () => { - loggedUser = defaultUser; - const { baseElement } = renderComponent(); - await waitFor(() => expect(baseElement).not.toHaveAttribute('aria-busy')); - expect(await screen.findByText('Tech magazines')).toBeInTheDocument(); - expect( - await screen.findByText('Description for Tech magazines'), - ).toBeInTheDocument(); - const [checkbox] = await screen.findAllByRole('checkbox'); - await waitFor(() => expect(checkbox).not.toBeChecked()); -}); - -it('should mutate update feed advanced settings', async () => { - loggedUser = defaultUser; - const updateAlerts = jest.fn(); - let advancedSettingsMutationCalled = false; - - const { baseElement } = renderComponent( - [createAdvancedSettingsAndFiltersMock()], - { - alerts: { filter: true }, - updateAlerts, - }, - ); - - await waitForNock(); - - await waitFor(async () => { - const data = await client.getQueryData( - getFeedSettingsQueryKey(defaultUser), - ); - expect(data).toBeTruthy(); - }); - - const param = { id: 2, enabled: false }; - - mockGraphQL({ - request: { - query: UPDATE_ADVANCED_SETTINGS_FILTERS_MUTATION, - variables: { settings: [param] }, - }, - result: () => { - advancedSettingsMutationCalled = true; - return { data: { advancedSettings: [{ id: 1, enabled: false }, param] } }; - }, - }); - - const checkbox = (await screen.findAllByRole('checkbox'))[1]; - fireEvent.click(checkbox); - - await waitFor(() => expect(baseElement).not.toHaveAttribute('aria-busy')); - await waitFor(() => expect(advancedSettingsMutationCalled).toBeTruthy()); - await waitFor(() => expect(updateAlerts).toBeCalled()); - await waitFor(() => expect(checkbox).not.toBeChecked()); -}); diff --git a/packages/shared/src/components/filters/AdvancedSettings.tsx b/packages/shared/src/components/filters/AdvancedSettings.tsx deleted file mode 100644 index a0571cd99a..0000000000 --- a/packages/shared/src/components/filters/AdvancedSettings.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import type { ReactElement } from 'react'; -import React, { useMemo } from 'react'; -import useFeedSettings from '../../hooks/useFeedSettings'; -import { FilterSwitch } from './FilterSwitch'; -import { useAdvancedSettings } from '../../hooks/feed'; -import { getContentCurationList, getContentSourceList } from './helpers'; - -const ADVANCED_SETTINGS_KEY = 'advancedSettings'; - -function AdvancedSettingsFilter(): ReactElement { - const { isLoading, advancedSettings } = useFeedSettings(); - const { - selectedSettings, - onToggleSettings, - checkSourceBlocked, - onToggleSource, - } = useAdvancedSettings(); - - const contentSourceList = useMemo( - () => getContentSourceList(advancedSettings), - [advancedSettings], - ); - - const contentCurationList = useMemo( - () => getContentCurationList(advancedSettings), - [advancedSettings], - ); - - return ( -
- {contentSourceList?.map(({ id, title, description, options }) => ( - onToggleSource(options.source)} - /> - ))} - {contentCurationList?.map( - ({ id, title, description, defaultEnabledState }) => ( - onToggleSettings(id, defaultEnabledState)} - /> - ), - )} -
- ); -} - -export default AdvancedSettingsFilter; diff --git a/packages/shared/src/components/filters/BlockedFilter.spec.tsx b/packages/shared/src/components/filters/BlockedFilter.spec.tsx deleted file mode 100644 index 12b61a9c7e..0000000000 --- a/packages/shared/src/components/filters/BlockedFilter.spec.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import nock from 'nock'; -import React from 'react'; -import type { RenderResult } from '@testing-library/react'; -import { render, screen, waitFor } from '@testing-library/react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import defaultUser from '../../../__tests__/fixture/loggedUser'; -import AuthContext from '../../contexts/AuthContext'; -import type { MockedGraphQLResponse } from '../../../__tests__/helpers/graphql'; -import { mockGraphQL } from '../../../__tests__/helpers/graphql'; -import type { LoggedUser } from '../../lib/user'; -import type { - FeedSettings, - AllTagCategoriesData, -} from '../../graphql/feedSettings'; -import { FEED_SETTINGS_QUERY } from '../../graphql/feedSettings'; -import BlockedFilter from './BlockedFilter'; - -const showLogin = jest.fn(); -const setUnblockItem = jest.fn(); - -beforeEach(() => { - jest.restoreAllMocks(); - jest.restoreAllMocks(); - jest.clearAllMocks(); - nock.cleanAll(); -}); - -const createAllBlockedTagsAndSourcesMock = ( - feedSettings: FeedSettings = { - includeTags: ['react', 'golang'], - blockedTags: ['javascript'], - excludeSources: [ - { - id: 'newstack', - image: - 'https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/newstack', - name: 'The New Stack', - }, - ], - }, -): MockedGraphQLResponse => ({ - request: { query: FEED_SETTINGS_QUERY }, - result: { - data: { - feedSettings, - }, - }, -}); - -let client: QueryClient; - -const renderComponent = ( - mocks: MockedGraphQLResponse[] = [createAllBlockedTagsAndSourcesMock()], - user: LoggedUser = defaultUser, -): RenderResult => { - client = new QueryClient(); - mocks.forEach(mockGraphQL); - return render( - - - - - , - ); -}; - -it('should show blocked tags', async () => { - const { baseElement } = renderComponent(); - await waitFor(() => expect(baseElement).not.toHaveAttribute('aria-busy')); - expect(await screen.findByText('#javascript')).toBeInTheDocument(); -}); - -it('should show blocked sources', async () => { - const { baseElement } = renderComponent(); - await waitFor(() => expect(baseElement).not.toHaveAttribute('aria-busy')); - expect(await screen.findByText('The New Stack')).toBeInTheDocument(); -}); - -it('should show unblock popup on option click', async () => { - const { baseElement } = renderComponent(); - - await waitFor(() => expect(baseElement).not.toHaveAttribute('aria-busy')); - - const el = await screen.findByLabelText('Unblock tag'); - el.click(); - - await waitFor(() => expect(setUnblockItem).toBeCalled()); -}); - -it('should show unblock popup on source unblock click', async () => { - const { baseElement } = renderComponent(); - - await waitFor(() => expect(baseElement).not.toHaveAttribute('aria-busy')); - - const el = await screen.findByLabelText('Unblock source'); - el.click(); - - await waitFor(() => expect(setUnblockItem).toBeCalled()); -}); diff --git a/packages/shared/src/components/filters/BlockedFilter.tsx b/packages/shared/src/components/filters/BlockedFilter.tsx deleted file mode 100644 index d3be3964f4..0000000000 --- a/packages/shared/src/components/filters/BlockedFilter.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import type { ReactElement } from 'react'; -import React from 'react'; -import dynamic from 'next/dynamic'; -import type { FilterMenuProps } from './common'; -import SourceItemList from './SourceItemList'; -import TagItemList from './TagItemList'; -import useFeedSettings from '../../hooks/useFeedSettings'; -import useTagAndSource from '../../hooks/useTagAndSource'; -import { BlockIcon } from '../icons'; -import { Origin } from '../../lib/log'; -import { - Typography, - TypographyColor, - TypographyType, -} from '../typography/Typography'; -import { usePlusSubscription } from '../../hooks'; - -const BlockedWords = dynamic(() => - import(/* webpackChunkName: "blockedWords" */ './BlockedWords').then( - (mod) => mod.BlockedWords, - ), -); - -export default function BlockedFilter({ - onUnblockItem, -}: FilterMenuProps): ReactElement { - const { showPlusSubscription } = usePlusSubscription(); - const { feedSettings, isLoading } = useFeedSettings(); - - const { onUnblockTags, onUnblockSource } = useTagAndSource({ - origin: Origin.BlockedFilter, - }); - - const tagItemAction = (event: React.MouseEvent, tag: string) => { - onUnblockItem({ - tag, - action: () => onUnblockTags({ tags: [tag] }), - }); - }; - - const sourceItemAction = (source) => { - onUnblockItem({ - source, - action: () => onUnblockSource({ source }), - }); - }; - - return ( -
- - Customize your feed by blocking what you don’t want to see. Remove{' '} - {showPlusSubscription ? 'specific words,' : undefined} tags or sources - to create a more personalized and focused experience. - - - {showPlusSubscription ? : undefined} - -
-

Blocked tags

- } - /> -
- -
-

Blocked sources

- -
-
- ); -} diff --git a/packages/shared/src/components/layout/common.tsx b/packages/shared/src/components/layout/common.tsx index 6508902794..934131140e 100644 --- a/packages/shared/src/components/layout/common.tsx +++ b/packages/shared/src/components/layout/common.tsx @@ -30,9 +30,6 @@ import { ToggleClickbaitShield } from '../buttons/ToggleClickbaitShield'; import { Origin } from '../../lib/log'; import { useAuthContext } from '../../contexts/AuthContext'; import useCustomDefaultFeed from '../../hooks/feed/useCustomDefaultFeed'; -import { useLazyModal } from '../../hooks/useLazyModal'; -import { LazyModal } from '../modals/common/types'; -import { DeleteCustomFeed } from '../buttons/DeleteCustomFeed'; type State = [T, Dispatch>]; @@ -75,11 +72,9 @@ export const SearchControlHeader = ({ const isLaptop = useViewSize(ViewSize.Laptop); const isMobile = useViewSize(ViewSize.MobileL); const { streak, isLoading, isStreaksEnabled } = useReadingStreak(); - const { showPlusSubscription, isEnrolledNotPlus, isPlus } = - usePlusSubscription(); + const { showPlusSubscription } = usePlusSubscription(); const { user } = useAuthContext(); const { isCustomDefaultFeed, defaultFeedId } = useCustomDefaultFeed(); - const { openModal } = useLazyModal(); if (isMobile) { return null; @@ -94,25 +89,17 @@ export const SearchControlHeader = ({ buttonVariant: isLaptop ? ButtonVariant.Float : ButtonVariant.Tertiary, }; - const feedsWithActions = [SharedFeedPage.MyFeed]; - if (showPlusSubscription) { - feedsWithActions.push(SharedFeedPage.Custom, SharedFeedPage.CustomForm); - } + const feedsWithActions = [ + SharedFeedPage.MyFeed, + SharedFeedPage.Custom, + SharedFeedPage.CustomForm, + ]; const actionButtons = [ feedsWithActions.includes(feedName as SharedFeedPage) ? ( { - if (isEnrolledNotPlus && feedName === SharedFeedPage.Custom) { - openModal({ - type: LazyModal.AdvancedCustomFeedSoon, - props: {}, - }); - - return; - } - if (isCustomDefaultFeed && router.pathname === '/') { router.push(`${webappUrl}feeds/${defaultFeedId}/edit`); } else { @@ -157,9 +144,6 @@ export const SearchControlHeader = ({ key="toggle-clickbait-shield" /> ) : null, - !isPlus && feedName === SharedFeedPage.Custom ? ( - - ) : null, ]; const actions = actionButtons.filter((button) => !!button); diff --git a/packages/shared/src/components/modals/common.tsx b/packages/shared/src/components/modals/common.tsx index aa1702927c..f0f6af6364 100644 --- a/packages/shared/src/components/modals/common.tsx +++ b/packages/shared/src/components/modals/common.tsx @@ -180,13 +180,6 @@ const TopReaderBadgeModal = dynamic( ), ); -const AdvancedCustomFeedSoonModal = dynamic( - () => - import( - /* webpackChunkName: "advancedCustomFeedSoonModal" */ './soon/AdvancedCustomFeedSoonModal' - ), -); - const BookmarkFolderSoonModal = dynamic( () => import( @@ -252,7 +245,6 @@ export const modals = { [LazyModal.PostModeration]: PostModerationModal, [LazyModal.NewSquad]: NewSquadModal, [LazyModal.TopReaderBadge]: TopReaderBadgeModal, - [LazyModal.AdvancedCustomFeedSoon]: AdvancedCustomFeedSoonModal, [LazyModal.BookmarkFolderSoon]: BookmarkFolderSoonModal, [LazyModal.BookmarkFolder]: BookmarkFolderModal, [LazyModal.ClickbaitShield]: ClickbaitShieldModal, diff --git a/packages/shared/src/components/modals/common/types.ts b/packages/shared/src/components/modals/common/types.ts index f5e9f32c70..00832f1e0b 100644 --- a/packages/shared/src/components/modals/common/types.ts +++ b/packages/shared/src/components/modals/common/types.ts @@ -55,7 +55,6 @@ export enum LazyModal { PostModeration = 'postModeration', NewSquad = 'newSquad', TopReaderBadge = 'topReaderBadge', - AdvancedCustomFeedSoon = 'advancedCustomFeedSoon', BookmarkFolderSoon = 'bookmarkFolderSoon', BookmarkFolder = 'bookmarkFolder', ClickbaitShield = 'clickbaitShield', diff --git a/packages/shared/src/components/modals/soon/AdvancedCustomFeedSoonModal.tsx b/packages/shared/src/components/modals/soon/AdvancedCustomFeedSoonModal.tsx deleted file mode 100644 index 7c60072bd7..0000000000 --- a/packages/shared/src/components/modals/soon/AdvancedCustomFeedSoonModal.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import type { ReactElement } from 'react'; -import React from 'react'; -import type { ModalProps } from '../common/Modal'; -import { Modal } from '../common/Modal'; -import { ModalClose } from '../common/ModalClose'; -import { ButtonSize, ButtonVariant } from '../../buttons/common'; -import { Image } from '../../image/Image'; -import { - Typography, - TypographyColor, - TypographyType, -} from '../../typography/Typography'; -import { Button } from '../../buttons/Button'; -import { advancedCustomFeedSoonImage } from '../../../lib/image'; -import { DevPlusIcon } from '../../icons'; -import { usePlusSubscription } from '../../../hooks/usePlusSubscription'; -import { LogEvent, TargetId } from '../../../lib/log'; -import { webappUrl } from '../../../lib/constants'; - -export type SlackIntegrationModalProps = Omit; - -const AdvancedCustomFeedSoonModal = ({ - ...props -}: SlackIntegrationModalProps): ReactElement => { - const { logSubscriptionEvent, isPlus } = usePlusSubscription(); - - return ( - - - -
- Advanced Custom Feeds -
- - Advanced Custom Feeds - - - Choose your tags, sources, content types, and languages. Enjoy diverse - sorting options, block unwanted words, and customize your feed, your - rules, your way. - - {!isPlus && ( -
- - Upgrade to daily.dev Plus today, get an early adopter discount and - be among the first to experience it! - - -
- )} -
-
- ); -}; - -export default AdvancedCustomFeedSoonModal; diff --git a/packages/shared/src/components/sidebar/sections/CustomFeedSection.tsx b/packages/shared/src/components/sidebar/sections/CustomFeedSection.tsx index c159fb83d5..99132afc67 100644 --- a/packages/shared/src/components/sidebar/sections/CustomFeedSection.tsx +++ b/packages/shared/src/components/sidebar/sections/CustomFeedSection.tsx @@ -3,12 +3,10 @@ import React, { useMemo } from 'react'; import type { SidebarMenuItem } from '../common'; import { HashtagIcon, PlusIcon } from '../../icons'; import { Section } from '../Section'; -import { customFeedsPlusDate, webappUrl } from '../../../lib/constants'; +import { webappUrl } from '../../../lib/constants'; import { useFeeds, usePlusSubscription } from '../../../hooks'; import { SidebarSettingsFlags } from '../../../graphql/settings'; import type { SidebarSectionProps } from './common'; -import { useLazyModal } from '../../../hooks/useLazyModal'; -import { LazyModal } from '../../modals/common/types'; import useCustomDefaultFeed from '../../../hooks/feed/useCustomDefaultFeed'; import { isExtension } from '../../../lib/func'; @@ -18,8 +16,7 @@ export const CustomFeedSection = ({ ...defaultRenderSectionProps }: SidebarSectionProps): ReactElement => { const { feeds } = useFeeds(); - const { openModal } = useLazyModal(); - const { showPlusSubscription, isPlus } = usePlusSubscription(); + const { showPlusSubscription } = usePlusSubscription(); const { defaultFeedId } = useCustomDefaultFeed(); const menuItems: SidebarMenuItem[] = useMemo(() => { @@ -56,55 +53,40 @@ export const CustomFeedSection = ({ secondary={defaultRenderSectionProps.activePage === feedPath} /> ), - action: ( - event: React.MouseEvent, - ) => { - if ( - showPlusSubscription && - !isPlus && - new Date(feed.node.createdAt) > customFeedsPlusDate - ) { - event.preventDefault(); - - openModal({ type: LazyModal.AdvancedCustomFeedSoon, props: {} }); - } - }, }; }) ?? []; return [ ...customFeeds, - { - icon: () => ( -
- -
- ), - title: 'Custom feed', - path: `${webappUrl}feeds/new`, - requiresLogin: true, - isForcedClickable: true, - action: ( - event: React.MouseEvent, - ) => { - if (showPlusSubscription && !isPlus) { - event.preventDefault(); - - openModal({ type: LazyModal.AdvancedCustomFeedSoon, props: {} }); + showPlusSubscription + ? { + icon: () => ( +
+ +
+ ), + title: 'Custom feed', + path: `${webappUrl}feeds/new`, + requiresLogin: true, + isForcedClickable: true, } - }, - }, + : undefined, ].filter(Boolean); }, [ defaultRenderSectionProps.activePage, feeds?.edges, - showPlusSubscription, - openModal, - isPlus, defaultFeedId, onNavTabClick, + showPlusSubscription, ]); + /** + * If there are no custom feeds and the user is not subscribed to Plus don't show this section + */ + if (!menuItems.length && !showPlusSubscription) { + return null; + } + return (
{ type: T; @@ -117,9 +118,13 @@ export default function useFeed( const { logEvent } = useLogContext(); const { query, variables, options = {}, settings, onEmptyFeed } = params; const { user, tokenRefreshed } = useContext(AuthContext); - const { isPlus } = usePlusSubscription(); + const { showPlusSubscription, isPlus } = usePlusSubscription(); const queryClient = useQueryClient(); const isFeedPreview = feedQueryKey?.[0] === RequestKey.FeedPreview; + const avoidRetry = + params?.settings?.feedName === SharedFeedPage.Custom && + showPlusSubscription && + !isPlus; const feedQuery = useInfiniteQuery({ queryKey: feedQueryKey, @@ -155,6 +160,7 @@ export default function useFeed( enabled: query && tokenRefreshed, refetchOnReconnect: false, refetchOnWindowFocus: false, + retry: avoidRetry ? false : 3, initialPageParam: '', getNextPageParam: ({ page }) => getNextPageParam(page?.pageInfo), }); diff --git a/packages/webapp/pages/feeds/[slugOrId]/index.tsx b/packages/webapp/pages/feeds/[slugOrId]/index.tsx index 6e354e6512..e00277f3b4 100644 --- a/packages/webapp/pages/feeds/[slugOrId]/index.tsx +++ b/packages/webapp/pages/feeds/[slugOrId]/index.tsx @@ -2,12 +2,9 @@ import type { ReactElement } from 'react'; import React, { useEffect, useMemo } from 'react'; import type { NextSeoProps } from 'next-seo/lib/types'; import { NextSeo } from 'next-seo'; -import { useFeeds, usePlusSubscription } from '@dailydotdev/shared/src/hooks'; +import { useFeeds } from '@dailydotdev/shared/src/hooks'; import { useRouter } from 'next/router'; -import { - customFeedsPlusDate, - webappUrl, -} from '@dailydotdev/shared/src/lib/constants'; +import { webappUrl } from '@dailydotdev/shared/src/lib/constants'; import { defaultOpenGraph, defaultSeo } from '../../../next-seo'; import { getMainFeedLayout, @@ -18,7 +15,6 @@ import { getTemplatedTitle } from '../../../components/layouts/utils'; const FeedPage = (): ReactElement => { const router = useRouter(); const { feeds } = useFeeds(); - const { isPlus } = usePlusSubscription(); const feed = useMemo(() => { return feeds?.edges.find(({ node }) => node.id === router.query.slugOrId) @@ -28,14 +24,8 @@ const FeedPage = (): ReactElement => { useEffect(() => { if (!feed) { router.replace(webappUrl); - - return; - } - - if (!isPlus && new Date(feed.createdAt) > customFeedsPlusDate) { - router.replace(webappUrl); } - }, [router, isPlus, feed]); + }, [router, feed]); const seo: NextSeoProps = { title: getTemplatedTitle(