From 364616884c53ffd9159532531eaf632b32a90747 Mon Sep 17 00:00:00 2001 From: idoshamun <1993245+idoshamun@users.noreply.github.com> Date: Sun, 13 Sep 2026 20:36:14 +0000 Subject: [PATCH] fix(profile): respect preview mode on the activity pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The activity section on the profile already swaps the owner empty state for the visitor one in preview mode, but the standalone posts, replies and upvoted pages still compared the logged user to the profile owner directly. The preview toggle lives in the profile sidebar, which renders on those pages too, so an owner with no posts toggled preview and was still offered "New post" — reading as if visitors could post on their profile. They now take `isOwner` from useProfilePreview, the same source the activity section uses, and the "Show More" links carry the preview flag so switching to the full list does not silently leave preview mode. The replies page spec gained a router mock of its own; its fixtures were also brought up to the current types, which the strict-changed guard checks once the file is touched. Co-Authored-By: Claude Opus 5 (1M context) --- .../profile/components/Activity.helpers.tsx | 4 +- .../features/profile/components/Activity.tsx | 2 + .../profile/components/ActivityPostsTab.tsx | 3 + .../profile/components/ActivityRepliesTab.tsx | 1 + .../profile/components/ActivityUpvotedTab.tsx | 3 + .../webapp/__tests__/ProfilePostsPage.tsx | 29 ++++++++++ .../webapp/__tests__/ProfileRepliesPage.tsx | 58 +++++++++++++++++-- .../webapp/__tests__/ProfileUpvotedPage.tsx | 29 ++++++++++ packages/webapp/pages/[userId]/posts.tsx | 9 ++- packages/webapp/pages/[userId]/replies.tsx | 9 ++- packages/webapp/pages/[userId]/upvoted.tsx | 9 ++- 11 files changed, 135 insertions(+), 21 deletions(-) diff --git a/packages/shared/src/features/profile/components/Activity.helpers.tsx b/packages/shared/src/features/profile/components/Activity.helpers.tsx index 8eecd9ba726..caae4df2690 100644 --- a/packages/shared/src/features/profile/components/Activity.helpers.tsx +++ b/packages/shared/src/features/profile/components/Activity.helpers.tsx @@ -107,9 +107,11 @@ export const getUserPath = ( username: string | undefined, userId: string | undefined, path: string, + isPreviewMode?: boolean, ): string => { const userIdentifier = username || userId; - return `/${userIdentifier}${path}`; + const query = isPreviewMode ? '?preview=true' : ''; + return `/${userIdentifier}${path}${query}`; }; export const renderEmptyScreen = ( diff --git a/packages/shared/src/features/profile/components/Activity.tsx b/packages/shared/src/features/profile/components/Activity.tsx index 8dc16f0b154..e1c7ef736e4 100644 --- a/packages/shared/src/features/profile/components/Activity.tsx +++ b/packages/shared/src/features/profile/components/Activity.tsx @@ -33,6 +33,7 @@ export const Activity = ({ user }: ActivityProps): ReactElement | null => { { diff --git a/packages/shared/src/features/profile/components/ActivityRepliesTab.tsx b/packages/shared/src/features/profile/components/ActivityRepliesTab.tsx index 330c82ab9d8..8a205ab902b 100644 --- a/packages/shared/src/features/profile/components/ActivityRepliesTab.tsx +++ b/packages/shared/src/features/profile/components/ActivityRepliesTab.tsx @@ -136,6 +136,7 @@ export const ActivityRepliesTab = ({ user?.username, user?.id, activityTabs[ActivityTabIndex.Replies].path, + isPreviewMode, )} passHref > diff --git a/packages/shared/src/features/profile/components/ActivityUpvotedTab.tsx b/packages/shared/src/features/profile/components/ActivityUpvotedTab.tsx index ce0a54972e8..89477e82cc7 100644 --- a/packages/shared/src/features/profile/components/ActivityUpvotedTab.tsx +++ b/packages/shared/src/features/profile/components/ActivityUpvotedTab.tsx @@ -24,6 +24,7 @@ import { HorizontalFeedWithContext } from './HorizontalFeedWithContext'; export const ActivityUpvotedTab = ({ userId, isSameUser, + isPreviewMode, userName, user, selectedTab, @@ -31,6 +32,7 @@ export const ActivityUpvotedTab = ({ }: { userId: string; isSameUser: boolean; + isPreviewMode: boolean; userName: string; user: PublicProfile; selectedTab: string; @@ -82,6 +84,7 @@ export const ActivityUpvotedTab = ({ user?.username, user?.id, activityTabs[ActivityTabIndex.Upvoted].path, + isPreviewMode, )} passHref > diff --git a/packages/webapp/__tests__/ProfilePostsPage.tsx b/packages/webapp/__tests__/ProfilePostsPage.tsx index aa8d9852f7a..a0e64f83420 100644 --- a/packages/webapp/__tests__/ProfilePostsPage.tsx +++ b/packages/webapp/__tests__/ProfilePostsPage.tsx @@ -137,3 +137,32 @@ it('should show different empty screen when visiting your profile', async () => const el = await screen.findByText('New post'); expect(el).toBeInTheDocument(); }); + +it('should not offer the owner a new post CTA in preview mode', async () => { + jest.mocked(useRouter).mockImplementation( + () => + ({ + pathname: '/[userId]/posts', + query: { userId: 'dailydotdev', preview: 'true' }, + isFallback: false, + } as unknown as NextRouter), + ); + renderComponent( + [ + createFeedMock({ + pageInfo: { + hasNextPage: true, + endCursor: '', + }, + edges: [], + }), + ], + {}, + defaultProfile as unknown as LoggedUser, + ); + await waitForNock(); + expect( + await screen.findByText("Daily Dev hasn't posted yet"), + ).toBeInTheDocument(); + expect(screen.queryByText('New post')).not.toBeInTheDocument(); +}); diff --git a/packages/webapp/__tests__/ProfileRepliesPage.tsx b/packages/webapp/__tests__/ProfileRepliesPage.tsx index 0bee95947b1..c7c0e275f82 100644 --- a/packages/webapp/__tests__/ProfileRepliesPage.tsx +++ b/packages/webapp/__tests__/ProfileRepliesPage.tsx @@ -4,6 +4,7 @@ import { render, screen } from '@testing-library/react'; import type { LoggedUser, PublicProfile, + UserSocialLink, } from '@dailydotdev/shared/src/lib/user'; import nock from 'nock'; import { QueryClient } from '@tanstack/react-query'; @@ -20,11 +21,26 @@ import type { Author, } from '@dailydotdev/shared/src/graphql/comments'; import { USER_COMMENTS_QUERY } from '@dailydotdev/shared/src/graphql/comments'; +import type { NextRouter } from 'next/router'; +import { useRouter } from 'next/router'; import ProfilePage from '../pages/[userId]/replies'; +jest.mock('next/router', () => ({ + useRouter: jest.fn(), +})); + beforeEach(() => { nock.cleanAll(); jest.clearAllMocks(); + + jest.mocked(useRouter).mockImplementation( + () => + ({ + pathname: '/', + query: {}, + isFallback: false, + } as unknown as NextRouter), + ); }); const defaultProfile: PublicProfile = { @@ -37,10 +53,12 @@ const defaultProfile: PublicProfile = { cover: 'https://daily.dev/cover.png', bio: 'The best company!', createdAt: '2020-08-26T13:04:35.000Z', - twitter: 'dailydotdev', - github: 'dailydotdev', - hashnode: 'dailydotdev', - portfolio: 'https://daily.dev/?key=vaue', + socialLinks: [ + { platform: 'twitter', url: 'https://x.com/dailydotdev' }, + { platform: 'github', url: 'https://github.com/dailydotdev' }, + { platform: 'hashnode', url: 'https://dailydotdev.hashnode.dev' }, + { platform: 'portfolio', url: 'https://daily.dev/?key=vaue' }, + ] as UserSocialLink[], permalink: 'https://daily.dev/dailydotdev', }; @@ -56,6 +74,7 @@ export const defaultCommentsPage: Connection = { createdAt: '2020-07-26T13:04:35.000Z', content: 'My comment', numUpvotes: 50, + numAwards: 0, id: 'c1', contentHtml: 'My comment', post: defaultPost, @@ -93,7 +112,7 @@ const renderComponent = ( mocks.forEach(mockGraphQL); return render( - + , ); }; @@ -145,3 +164,32 @@ it('should show different empty screen when visiting your profile', async () => const el = await screen.findByText('Explore posts'); expect(el).toBeInTheDocument(); }); + +it('should show the visitor empty screen to the owner in preview mode', async () => { + jest.mocked(useRouter).mockImplementation( + () => + ({ + pathname: '/[userId]/replies', + query: { userId: 'dailydotdev', preview: 'true' }, + isFallback: false, + } as unknown as NextRouter), + ); + renderComponent( + [ + createCommentsMock({ + pageInfo: { + hasNextPage: true, + endCursor: '', + }, + edges: [], + }), + ], + {}, + defaultProfile as unknown as LoggedUser, + ); + await waitForNock(); + expect( + await screen.findByText("Daily Dev hasn't replied to any post yet"), + ).toBeInTheDocument(); + expect(screen.queryByText('Explore posts')).not.toBeInTheDocument(); +}); diff --git a/packages/webapp/__tests__/ProfileUpvotedPage.tsx b/packages/webapp/__tests__/ProfileUpvotedPage.tsx index 3f8f77df2f7..5c9383852cc 100644 --- a/packages/webapp/__tests__/ProfileUpvotedPage.tsx +++ b/packages/webapp/__tests__/ProfileUpvotedPage.tsx @@ -137,3 +137,32 @@ it('should show different empty screen when visiting your profile', async () => const el = await screen.findByText('Explore posts'); expect(el).toBeInTheDocument(); }); + +it('should show the visitor empty screen to the owner in preview mode', async () => { + jest.mocked(useRouter).mockImplementation( + () => + ({ + pathname: '/[userId]/upvoted', + query: { userId: 'dailydotdev', preview: 'true' }, + isFallback: false, + } as unknown as NextRouter), + ); + renderComponent( + [ + createFeedMock({ + pageInfo: { + hasNextPage: true, + endCursor: '', + }, + edges: [], + }), + ], + {}, + defaultProfile as unknown as LoggedUser, + ); + await waitForNock(); + expect( + await screen.findByText("Daily Dev hasn't upvoted yet"), + ).toBeInTheDocument(); + expect(screen.queryByText('Explore posts')).not.toBeInTheDocument(); +}); diff --git a/packages/webapp/pages/[userId]/posts.tsx b/packages/webapp/pages/[userId]/posts.tsx index 536596f24c3..ffab7f79e08 100644 --- a/packages/webapp/pages/[userId]/posts.tsx +++ b/packages/webapp/pages/[userId]/posts.tsx @@ -1,5 +1,5 @@ import type { ReactElement } from 'react'; -import React, { useContext } from 'react'; +import React from 'react'; import { link } from '@dailydotdev/shared/src/lib/links'; import { AUTHOR_FEED_QUERY } from '@dailydotdev/shared/src/graphql/feed'; import type { FeedProps } from '@dailydotdev/shared/src/components/Feed'; @@ -8,7 +8,7 @@ import { OtherFeedPage } from '@dailydotdev/shared/src/lib/query'; import { MyProfileEmptyScreen } from '@dailydotdev/shared/src/components/profile/MyProfileEmptyScreen'; import { ProfileEmptyScreen } from '@dailydotdev/shared/src/components/profile/ProfileEmptyScreen'; import { cloudinaryCharmNoPosts } from '@dailydotdev/shared/src/lib/image'; -import AuthContext from '@dailydotdev/shared/src/contexts/AuthContext'; +import { useProfilePreview } from '@dailydotdev/shared/src/hooks/profile/useProfilePreview'; import { useFeedLayout } from '@dailydotdev/shared/src/hooks'; import classNames from 'classnames'; import { NextSeo } from 'next-seo'; @@ -35,14 +35,13 @@ const ProfilePostsPage = ({ user, noindex, }: ProfileLayoutProps): ReactElement | null => { - const { user: loggedUser } = useContext(AuthContext); + const { isOwner } = useProfilePreview(user); const { shouldUseListFeedLayout } = useFeedLayout(); if (!user) { return null; } - const isSameUser = loggedUser?.id === user.id; const userId = user.id; const feedProps: FeedProps = { feedName: OtherFeedPage.Author, @@ -52,7 +51,7 @@ const ProfilePostsPage = ({ userId, }, disableAds: true, - emptyScreen: isSameUser ? ( + emptyScreen: isOwner ? ( { - const { user: loggedUser } = useContext(AuthContext); + const { isOwner } = useProfilePreview(user); if (!user) { return null; } - const isSameUser = loggedUser?.id === user.id; const userId = user.id; - const emptyScreen = isSameUser ? ( + const emptyScreen = isOwner ? ( { - const { user: loggedUser } = useContext(AuthContext); + const { isOwner } = useProfilePreview(user); const { shouldUseListFeedLayout } = useFeedLayout(); if (!user) { return null; } - const isSameUser = loggedUser?.id === user.id; const userId = user.id; const feedProps: FeedProps = { feedName: OtherFeedPage.UserUpvoted, @@ -51,7 +50,7 @@ const ProfileUpvotedPage = ({ userId, }, disableAds: true, - emptyScreen: isSameUser ? ( + emptyScreen: isOwner ? (