diff --git a/packages/shared/src/components/filters/MyFeedHeading.spec.tsx b/packages/shared/src/components/filters/MyFeedHeading.spec.tsx
new file mode 100644
index 00000000000..64f9976a271
--- /dev/null
+++ b/packages/shared/src/components/filters/MyFeedHeading.spec.tsx
@@ -0,0 +1,254 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { useRouter } from 'next/router';
+import { useAuthContext } from '../../contexts/AuthContext';
+import { useActiveFeedNameContext } from '../../contexts';
+import { useSettingsContext } from '../../contexts/SettingsContext';
+import { useActions, useFeedLayout, useViewSize, ViewSize } from '../../hooks';
+import { useShortcutsUser } from '../../features/shortcuts/hooks/useShortcutsUser';
+import { ActionType } from '../../graphql/actions';
+import useCustomDefaultFeed from '../../hooks/feed/useCustomDefaultFeed';
+import { getHasSeenTags, setHasSeenTags } from '../../lib/feedSettings';
+import { SharedFeedPage } from '../utilities';
+import MyFeedHeading from './MyFeedHeading';
+
+jest.mock('next/router', () => ({
+ useRouter: jest.fn(),
+}));
+
+jest.mock('../../contexts/AuthContext', () => ({
+ useAuthContext: jest.fn(),
+}));
+
+jest.mock('../../contexts', () => ({
+ useActiveFeedNameContext: jest.fn(),
+}));
+
+jest.mock('../../contexts/SettingsContext', () => ({
+ useSettingsContext: jest.fn(),
+}));
+
+jest.mock('../../hooks', () => ({
+ useActions: jest.fn(),
+ useFeedLayout: jest.fn(),
+ useViewSize: jest.fn(),
+ ViewSize: {
+ MobileL: 'mobile',
+ Laptop: 'laptop',
+ },
+}));
+
+jest.mock('../../features/shortcuts/hooks/useShortcutsUser', () => ({
+ useShortcutsUser: jest.fn(),
+}));
+
+jest.mock('../../hooks/feed/useCustomDefaultFeed', () => ({
+ __esModule: true,
+ default: jest.fn(),
+}));
+
+jest.mock('../AlertDot', () => ({
+ AlertDot: ({ className }: { className?: string }) => (
+
+ ),
+ AlertColor: { Bun: 'bg-accent-bun-default' },
+}));
+
+jest.mock('../feeds/FeedSettingsButton', () => ({
+ FeedSettingsButton: ({
+ children,
+ onClick,
+ }: {
+ children: React.ReactNode;
+ onClick: () => void;
+ }) => (
+
+ ),
+}));
+
+jest.mock('../../lib/constants', () => ({
+ ...jest.requireActual('../../lib/constants'),
+ webappUrl: 'https://app.daily.dev/',
+ settingsUrl: 'https://app.daily.dev/settings',
+}));
+
+jest.mock('../../lib/feedSettings', () => ({
+ getHasSeenTags: jest.fn(),
+ setHasSeenTags: jest.fn(),
+}));
+
+const mockUseRouter = useRouter as jest.Mock;
+const mockUseAuthContext = useAuthContext as jest.Mock;
+const mockUseActiveFeedNameContext = useActiveFeedNameContext as jest.Mock;
+const mockUseSettingsContext = useSettingsContext as jest.Mock;
+const mockUseActions = useActions as jest.Mock;
+const mockUseFeedLayout = useFeedLayout as jest.Mock;
+const mockUseViewSize = useViewSize as jest.Mock;
+const mockUseShortcutsUser = useShortcutsUser as jest.Mock;
+const mockUseCustomDefaultFeed = useCustomDefaultFeed as jest.Mock;
+const mockGetHasSeenTags = getHasSeenTags as jest.Mock;
+const mockSetHasSeenTags = setHasSeenTags as jest.Mock;
+
+const push = jest.fn();
+const completeAction = jest.fn();
+
+const renderComponent = () => render();
+
+describe('MyFeedHeading', () => {
+ beforeEach(() => {
+ push.mockReset();
+ push.mockResolvedValue(true);
+ completeAction.mockReset();
+ completeAction.mockResolvedValue(undefined);
+ mockGetHasSeenTags.mockReset();
+ mockGetHasSeenTags.mockReturnValue(null);
+ mockSetHasSeenTags.mockReset();
+
+ mockUseRouter.mockReturnValue({
+ push,
+ pathname: '/',
+ query: {},
+ });
+ mockUseAuthContext.mockReturnValue({
+ user: { id: 'user-1' },
+ });
+ mockUseActiveFeedNameContext.mockReturnValue({
+ feedName: SharedFeedPage.MyFeed,
+ });
+ mockUseSettingsContext.mockReturnValue({
+ toggleShowTopSites: jest.fn(),
+ });
+ mockUseActions.mockReturnValue({
+ completeAction,
+ checkHasCompleted: jest.fn().mockReturnValue(false),
+ isActionsFetched: true,
+ });
+ mockUseFeedLayout.mockReturnValue({
+ shouldUseListFeedLayout: false,
+ });
+ mockUseViewSize.mockImplementation((size) => size === ViewSize.Laptop);
+ mockUseShortcutsUser.mockReturnValue({
+ isOldUserWithNoShortcuts: false,
+ showToggleShortcuts: false,
+ });
+ mockUseCustomDefaultFeed.mockReturnValue({
+ isCustomDefaultFeed: false,
+ defaultFeedId: 'user-1',
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('routes the home custom default feed to its edit page', async () => {
+ mockUseCustomDefaultFeed.mockReturnValue({
+ isCustomDefaultFeed: true,
+ defaultFeedId: 'feed-1',
+ });
+
+ renderComponent();
+
+ await userEvent.click(
+ screen.getByRole('button', { name: 'Feed settings' }),
+ );
+
+ expect(push).toHaveBeenCalledWith(
+ 'https://app.daily.dev/feeds/feed-1/edit',
+ );
+ });
+
+ it('routes the home For you feed to the user edit page with the tags tab open', async () => {
+ renderComponent();
+
+ await userEvent.click(
+ screen.getByRole('button', { name: 'Feed settings' }),
+ );
+
+ expect(push).toHaveBeenCalledWith(
+ 'https://app.daily.dev/feeds/user-1/edit?dview=tags',
+ );
+ });
+
+ it('routes the For you feed to the user edit page with the tags tab open', async () => {
+ mockUseRouter.mockReturnValue({
+ push,
+ pathname: '/my-feed',
+ query: {},
+ });
+
+ renderComponent();
+
+ await userEvent.click(
+ screen.getByRole('button', { name: 'Feed settings' }),
+ );
+
+ expect(push).toHaveBeenCalledWith(
+ 'https://app.daily.dev/feeds/user-1/edit?dview=tags',
+ );
+ });
+
+ it('routes custom feeds to their slug or id edit page', async () => {
+ mockUseRouter.mockReturnValue({
+ push,
+ pathname: '/feeds/[slugOrId]',
+ query: { slugOrId: 'feed-2' },
+ });
+ mockUseActiveFeedNameContext.mockReturnValue({
+ feedName: SharedFeedPage.Custom,
+ });
+
+ renderComponent();
+
+ await userEvent.click(
+ screen.getByRole('button', { name: 'Feed settings' }),
+ );
+
+ expect(push).toHaveBeenCalledWith(
+ 'https://app.daily.dev/feeds/feed-2/edit',
+ );
+ });
+
+ it('shows the tags reminder dot for the For you feed when tags were not seen yet', () => {
+ mockGetHasSeenTags.mockReturnValue(false);
+
+ renderComponent();
+
+ expect(screen.getByTestId('alert-dot')).toBeInTheDocument();
+ });
+
+ it('does not show the tags reminder dot for custom feeds', () => {
+ mockGetHasSeenTags.mockReturnValue(false);
+ mockUseRouter.mockReturnValue({
+ push,
+ pathname: '/feeds/[slugOrId]',
+ query: { slugOrId: 'feed-2' },
+ });
+ mockUseActiveFeedNameContext.mockReturnValue({
+ feedName: SharedFeedPage.Custom,
+ });
+
+ renderComponent();
+
+ expect(screen.queryByTestId('alert-dot')).not.toBeInTheDocument();
+ });
+
+ it('marks tags as seen before navigating from the For you feed settings button', async () => {
+ mockGetHasSeenTags.mockReturnValue(false);
+
+ renderComponent();
+
+ await userEvent.click(
+ screen.getByRole('button', { name: 'Feed settings' }),
+ );
+
+ expect(mockSetHasSeenTags).toHaveBeenCalledWith('user-1', true);
+ expect(completeAction).toHaveBeenCalledWith(ActionType.HasSeenTags);
+ expect(push).toHaveBeenCalledWith(
+ 'https://app.daily.dev/feeds/user-1/edit?dview=tags',
+ );
+ });
+});
diff --git a/packages/shared/src/components/filters/MyFeedHeading.tsx b/packages/shared/src/components/filters/MyFeedHeading.tsx
index 70f9a0d5cc9..c1d824c7e5a 100644
--- a/packages/shared/src/components/filters/MyFeedHeading.tsx
+++ b/packages/shared/src/components/filters/MyFeedHeading.tsx
@@ -1,5 +1,5 @@
import type { ReactElement } from 'react';
-import React, { useCallback, useMemo } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/router';
import { FilterIcon, PlusIcon } from '../icons';
import {
@@ -12,9 +12,13 @@ import { useActions, useFeedLayout, useViewSize, ViewSize } from '../../hooks';
import { ActionType } from '../../graphql/actions';
import { useSettingsContext } from '../../contexts/SettingsContext';
import { FeedSettingsButton } from '../feeds/FeedSettingsButton';
+import { AlertColor, AlertDot } from '../AlertDot';
+import { FeedSettingsMenu } from '../feeds/FeedSettings/types';
import { useShortcutsUser } from '../../features/shortcuts/hooks/useShortcutsUser';
import useCustomDefaultFeed from '../../hooks/feed/useCustomDefaultFeed';
+import { useAuthContext } from '../../contexts/AuthContext';
import { settingsUrl, webappUrl } from '../../lib/constants';
+import { getHasSeenTags, setHasSeenTags } from '../../lib/feedSettings';
import { SharedFeedPage } from '../utilities';
import { useActiveFeedNameContext } from '../../contexts';
@@ -26,7 +30,7 @@ function MyFeedHeading({
onOpenFeedFilters,
}: MyFeedHeadingProps): ReactElement {
const { push, pathname, query } = useRouter();
- const { completeAction } = useActions();
+ const { completeAction, checkHasCompleted, isActionsFetched } = useActions();
const { toggleShowTopSites } = useSettingsContext();
const { isOldUserWithNoShortcuts, showToggleShortcuts } = useShortcutsUser();
const isMobile = useViewSize(ViewSize.MobileL);
@@ -34,38 +38,88 @@ function MyFeedHeading({
const isLaptop = useViewSize(ViewSize.Laptop);
const { isCustomDefaultFeed, defaultFeedId } = useCustomDefaultFeed();
const { feedName } = useActiveFeedNameContext();
+ const { user } = useAuthContext();
+ const [hasSeenTagsState, setHasSeenTagsState] = useState(
+ null,
+ );
+
+ const hasSeenTagsAction =
+ isActionsFetched && checkHasCompleted(ActionType.HasSeenTags);
const editFeedUrl = useMemo(() => {
if (isCustomDefaultFeed && pathname === '/') {
return `${webappUrl}feeds/${defaultFeedId}/edit`;
}
+ if (feedName === SharedFeedPage.MyFeed && user?.id) {
+ return `${webappUrl}feeds/${user.id}/edit?dview=${FeedSettingsMenu.Tags}`;
+ }
+
if (feedName === SharedFeedPage.Custom) {
return `${webappUrl}feeds/${query.slugOrId}/edit`;
}
return `${settingsUrl}/feed/general`;
- }, [defaultFeedId, feedName, isCustomDefaultFeed, pathname, query]);
+ }, [defaultFeedId, feedName, isCustomDefaultFeed, pathname, query, user?.id]);
+
+ useEffect(() => {
+ if (!user?.id) {
+ setHasSeenTagsState(null);
+ return;
+ }
+
+ if (hasSeenTagsAction) {
+ setHasSeenTags(user.id, true);
+ setHasSeenTagsState(true);
+ return;
+ }
+
+ setHasSeenTagsState(getHasSeenTags(user.id));
+ }, [hasSeenTagsAction, user?.id]);
+
+ const shouldShowTagsReminder =
+ feedName === SharedFeedPage.MyFeed && hasSeenTagsState === false;
const onClick = useCallback(() => {
+ if (shouldShowTagsReminder && user?.id) {
+ setHasSeenTags(user.id, true);
+ setHasSeenTagsState(true);
+ completeAction(ActionType.HasSeenTags).catch(() => null);
+ }
+
onOpenFeedFilters?.();
return push(editFeedUrl);
- }, [editFeedUrl, onOpenFeedFilters, push]);
+ }, [
+ completeAction,
+ editFeedUrl,
+ onOpenFeedFilters,
+ push,
+ shouldShowTagsReminder,
+ user?.id,
+ ]);
return (
<>
- }
- iconPosition={
- shouldUseListFeedLayout ? ButtonIconPosition.Right : undefined
- }
- >
- {!isMobile ? 'Feed settings' : null}
-
+
+
}
+ iconPosition={
+ shouldUseListFeedLayout ? ButtonIconPosition.Right : undefined
+ }
+ >
+ {!isMobile ? 'Feed settings' : null}
+
+ {shouldShowTagsReminder && (
+
+ )}
+
{showToggleShortcuts && (