From 9b4faa4b70ccc6499dc707ce2210e35b11e1b992 Mon Sep 17 00:00:00 2001 From: AmarTrebinjac Date: Fri, 29 May 2026 08:38:23 +0000 Subject: [PATCH 1/2] feat: remove new indicator from quest button Drop the hasNewQuestRotations-driven "new" bubble and the supporting frontend plumbing (mark-viewed mutation, account-age check, dashboard field). The backend schema is left in place and can be cleaned up in a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/quest/QuestButton.spec.tsx | 180 +----------------- .../src/components/quest/QuestButton.tsx | 37 +--- packages/shared/src/graphql/quests.ts | 16 -- .../src/hooks/useMarkQuestRotationsViewed.ts | 53 ------ packages/webapp/lib/gameCenter.spec.ts | 2 +- 5 files changed, 9 insertions(+), 279 deletions(-) delete mode 100644 packages/shared/src/hooks/useMarkQuestRotationsViewed.ts diff --git a/packages/shared/src/components/quest/QuestButton.spec.tsx b/packages/shared/src/components/quest/QuestButton.spec.tsx index 4d3888494e7..ee15295ae60 100644 --- a/packages/shared/src/components/quest/QuestButton.spec.tsx +++ b/packages/shared/src/components/quest/QuestButton.spec.tsx @@ -1,11 +1,10 @@ import React from 'react'; import { useRouter } from 'next/router'; import { QueryClient } from '@tanstack/react-query'; -import { act, render, screen, waitFor, within } from '@testing-library/react'; +import { act, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import type { ReactElement, ReactNode } from 'react'; import { TestBootProvider } from '../../../__tests__/helpers/boot'; -import loggedUser from '../../../__tests__/fixture/loggedUser'; import type { QuestDashboard } from '../../graphql/quests'; import { QuestRewardType, @@ -16,7 +15,6 @@ import { } from '../../graphql/quests'; import { QuestButton } from './QuestButton'; import { useClaimQuestReward } from '../../hooks/useClaimQuestReward'; -import { useMarkQuestRotationsViewed } from '../../hooks/useMarkQuestRotationsViewed'; import { useQuestDashboard } from '../../hooks/useQuestDashboard'; import { usePlusSubscription } from '../../hooks/usePlusSubscription'; import useSubscription from '../../hooks/useSubscription'; @@ -40,10 +38,6 @@ jest.mock('../../hooks/useClaimQuestReward', () => ({ useClaimQuestReward: jest.fn(), })); -jest.mock('../../hooks/useMarkQuestRotationsViewed', () => ({ - useMarkQuestRotationsViewed: jest.fn(), -})); - jest.mock('../icons', () => { const actual = jest.requireActual('../icons'); @@ -183,8 +177,6 @@ jest.mock('../tooltip/Tooltip', () => { const mockUseQuestDashboard = useQuestDashboard as jest.Mock; const mockUseClaimQuestReward = useClaimQuestReward as jest.Mock; -const mockUseMarkQuestRotationsViewed = - useMarkQuestRotationsViewed as jest.Mock; const mockUseSubscription = useSubscription as jest.Mock; const mockUsePlusSubscription = usePlusSubscription as jest.Mock; const mockLogSubscriptionEvent = jest.fn(); @@ -199,7 +191,6 @@ const questDashboard = { }, currentStreak: 0, longestStreak: 0, - hasNewQuestRotations: false, daily: { regular: [ { @@ -230,6 +221,7 @@ const questDashboard = { plus: [], }, milestone: [], + intro: [], }; const renderComponent = ({ @@ -237,20 +229,17 @@ const renderComponent = ({ client = new QueryClient(), compact = false, log = {}, - auth = {}, }: { optOutLevelSystem?: boolean; client?: QueryClient; compact?: boolean; log?: Record; - auth?: Record; } = {}) => render( , @@ -271,10 +260,6 @@ beforeEach(() => { isPending: false, variables: undefined, }); - mockUseMarkQuestRotationsViewed.mockReturnValue({ - mutate: jest.fn(), - isPending: false, - }); mockUseSubscription.mockReset(); mockLogSubscriptionEvent.mockReset(); mockUsePlusSubscription.mockReturnValue({ @@ -621,129 +606,14 @@ describe('QuestButton', () => { }); }); - it('should not show a new indicator when the dashboard reports no new quest rotations', async () => { - renderComponent(); - - expect( - screen.getByRole('button', { - name: /Quests, level 7, 63% progress/i, - }), - ).toBeInTheDocument(); - expect( - screen.queryByTestId('quest-button-new-indicator'), - ).not.toBeInTheDocument(); - }); - - it('should show and clear the new quest indicator after a quest rotation update', async () => { - const client = new QueryClient(); - const establishedUser = { - ...loggedUser, - createdAt: new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString(), - }; - const subscriptions: Array<{ - query: string; - next?: () => unknown; - }> = []; - let questDashboardState = { - data: questDashboard, - isPending: false, - isError: false, - dataUpdatedAt: 1, - }; - const markQuestRotationsViewed = jest.fn(() => { - questDashboardState = { - ...questDashboardState, - data: { - ...questDashboardState.data, - hasNewQuestRotations: false, - }, - dataUpdatedAt: 3, - }; - }); - - mockUseQuestDashboard.mockImplementation(() => questDashboardState); - mockUseMarkQuestRotationsViewed.mockReturnValue({ - mutate: markQuestRotationsViewed, - isPending: false, - }); - mockUseSubscription.mockImplementation( - ( - request: () => { query: string }, - callbacks: { next?: () => unknown }, - ) => { - subscriptions.push({ - query: request().query, - next: callbacks.next, - }); - }, - ); - - const view = render( - - - , - ); - - questDashboardState = { - data: { - ...questDashboard, - hasNewQuestRotations: true, - daily: { - ...questDashboard.daily, - regular: [ - { - ...questDashboard.daily.regular[0], - rotationId: 'daily-quest-2', - }, - ], - }, - }, + it('should never render the new quest indicator bubble regardless of dashboard state', () => { + mockUseQuestDashboard.mockReturnValue({ + data: { ...questDashboard, hasNewQuestRotations: true }, isPending: false, isError: false, - dataUpdatedAt: 2, - }; - - subscriptions - .find( - (subscription) => - subscription.query === QUEST_ROTATION_UPDATE_SUBSCRIPTION, - ) - ?.next?.(); - - view.rerender( - - - , - ); - - expect( - screen.getByRole('button', { - name: /Quests, level 7, 63% progress, new quests available/i, - }), - ).toBeInTheDocument(); - expect( - screen.getByTestId('quest-button-new-indicator'), - ).toBeInTheDocument(); - expect(screen.getByTestId('quest-button-new-indicator')).toHaveTextContent( - 'new', - ); - expect(markQuestRotationsViewed).not.toHaveBeenCalled(); - - await userEvent.click( - screen.getByRole('button', { - name: /Quests, level 7, 63% progress, new quests available/i, - }), - ); - - await waitFor(() => { - expect(markQuestRotationsViewed).toHaveBeenCalledTimes(1); }); - view.rerender( - - - , - ); + renderComponent(); expect( screen.queryByTestId('quest-button-new-indicator'), @@ -1514,42 +1384,4 @@ describe('QuestButton', () => { exact: true, }); }); - - it('should not show new indicator for users created less than 24 hours ago', () => { - mockUseQuestDashboard.mockReturnValue({ - data: { ...questDashboard, hasNewQuestRotations: true }, - isPending: false, - isError: false, - }); - - const newUser = { - ...loggedUser, - createdAt: new Date(Date.now() - 12 * 60 * 60 * 1000).toISOString(), - }; - - renderComponent({ auth: { user: newUser } }); - - expect( - screen.queryByTestId('quest-button-new-indicator'), - ).not.toBeInTheDocument(); - }); - - it('should show new indicator for users created more than 24 hours ago', () => { - mockUseQuestDashboard.mockReturnValue({ - data: { ...questDashboard, hasNewQuestRotations: true }, - isPending: false, - isError: false, - }); - - const establishedUser = { - ...loggedUser, - createdAt: new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString(), - }; - - renderComponent({ auth: { user: establishedUser } }); - - expect( - screen.getByTestId('quest-button-new-indicator'), - ).toBeInTheDocument(); - }); }); diff --git a/packages/shared/src/components/quest/QuestButton.tsx b/packages/shared/src/components/quest/QuestButton.tsx index 8865a4d3344..3420e0d1fc3 100644 --- a/packages/shared/src/components/quest/QuestButton.tsx +++ b/packages/shared/src/components/quest/QuestButton.tsx @@ -31,7 +31,6 @@ import { QUEST_UPDATE_SUBSCRIPTION, } from '../../graphql/quests'; import { useClaimQuestReward } from '../../hooks/useClaimQuestReward'; -import { useMarkQuestRotationsViewed } from '../../hooks/useMarkQuestRotationsViewed'; import { useQuestDashboard } from '../../hooks/useQuestDashboard'; import { usePlusSubscription } from '../../hooks/usePlusSubscription'; import { useAuthContext } from '../../contexts/AuthContext'; @@ -758,7 +757,6 @@ export const QuestButton = ({ const level = data?.level?.level ?? 1; const levelProgress = data?.level ? getLevelProgress(data.level) : 0; const showLevelSystem = !optOutLevelSystem; - const { mutate: markQuestRotationsViewed } = useMarkQuestRotationsViewed(); const claimableCount = useMemo(() => { if (!data) { return 0; @@ -813,11 +811,6 @@ export const QuestButton = ({ const triggerVisualClassName = compact ? 'size-8' : 'size-10'; const triggerLevelClassName = compact ? 'typo-caption2' : 'typo-caption1'; const [isOpen, setIsOpen] = useState(false); - const isAccountOlderThan24Hours = - !!user?.createdAt && - Date.now() - new Date(user.createdAt).getTime() > 24 * 60 * 60 * 1000; - const hasNewQuestRotations = - (data?.hasNewQuestRotations ?? false) && isAccountOlderThan24Hours; const claimedStampRotationIdSet = useMemo( () => new Set(claimedStampRotationIds), [claimedStampRotationIds], @@ -836,14 +829,6 @@ export const QuestButton = ({ ); const scrollFadeRef = useScrollFade(); - useEffect(() => { - if (!isOpen || !hasNewQuestRotations) { - return; - } - - markQuestRotationsViewed(); - }, [hasNewQuestRotations, isOpen, markQuestRotationsViewed]); - const clearProgressTimers = useCallback(() => { progressTimersRef.current.forEach((timerId) => { window.clearTimeout(timerId); @@ -1228,12 +1213,8 @@ export const QuestButton = ({ showLevelSystem ? `Quests, level ${renderedLevel}, ${Math.round( renderedLevelProgress, - )}% progress${ - hasNewQuestRotations ? ', new quests available' : '' - }` - : `Quests${ - hasNewQuestRotations ? ', new quests available' : '' - }` + )}% progress` + : 'Quests' } > {showLevelSystem ? ( @@ -1292,20 +1273,6 @@ export const QuestButton = ({ )} - {hasNewQuestRotations && !claimableCount && ( - - new - - )} {claimableCount > 0 && ( { - const queryClient = useQueryClient(); - const { user } = useAuthContext(); - const { requestMethod } = useRequestProtocol(); - const questDashboardKey = generateQueryKey(RequestKey.QuestDashboard, user); - - return useMutation({ - mutationFn: async () => { - const result = await requestMethod( - MARK_QUEST_ROTATIONS_VIEWED_MUTATION, - ); - - return result.markQuestRotationsViewed; - }, - onMutate: () => { - const previousDashboard = - queryClient.getQueryData(questDashboardKey); - - queryClient.setQueryData( - questDashboardKey, - (currentDashboard) => { - if (!currentDashboard || !currentDashboard.hasNewQuestRotations) { - return currentDashboard; - } - - return { - ...currentDashboard, - hasNewQuestRotations: false, - }; - }, - ); - - return previousDashboard; - }, - onError: (_, __, previousDashboard) => { - if (previousDashboard) { - queryClient.setQueryData(questDashboardKey, previousDashboard); - } - }, - }); -}; - -export default useMarkQuestRotationsViewed; diff --git a/packages/webapp/lib/gameCenter.spec.ts b/packages/webapp/lib/gameCenter.spec.ts index 9ae632224da..46a2cdfa432 100644 --- a/packages/webapp/lib/gameCenter.spec.ts +++ b/packages/webapp/lib/gameCenter.spec.ts @@ -77,7 +77,6 @@ describe('game center helpers', () => { xpInLevel: 150, xpToNextLevel: 250, }, - hasNewQuestRotations: false, currentStreak: 4, longestStreak: 9, daily: { @@ -132,6 +131,7 @@ describe('game center helpers', () => { }, }), ], + intro: [], }; const summary = getQuestSummary(dashboard); From a5fa8cf0ee304b5336e9c0b3f2fde0d9426edfd5 Mon Sep 17 00:00:00 2001 From: AmarTrebinjac Date: Fri, 29 May 2026 08:43:52 +0000 Subject: [PATCH 2/2] chore: simplify quest rotation cleanup - Drop the misleading hasNewQuestRotations:true spread in the bubble guard test; the field no longer exists, so asserting the bubble never renders against the default dashboard is enough. - Remove orphaned QuestRotationUpdate / QuestRotationUpdateData types that only existed to describe the deleted rotation-viewed flow. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../shared/src/components/quest/QuestButton.spec.tsx | 8 +------- packages/shared/src/graphql/quests.ts | 11 ----------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/shared/src/components/quest/QuestButton.spec.tsx b/packages/shared/src/components/quest/QuestButton.spec.tsx index ee15295ae60..2c7c743576f 100644 --- a/packages/shared/src/components/quest/QuestButton.spec.tsx +++ b/packages/shared/src/components/quest/QuestButton.spec.tsx @@ -606,13 +606,7 @@ describe('QuestButton', () => { }); }); - it('should never render the new quest indicator bubble regardless of dashboard state', () => { - mockUseQuestDashboard.mockReturnValue({ - data: { ...questDashboard, hasNewQuestRotations: true }, - isPending: false, - isError: false, - }); - + it('should never render the new quest indicator bubble', () => { renderComponent(); expect( diff --git a/packages/shared/src/graphql/quests.ts b/packages/shared/src/graphql/quests.ts index ffde71db217..249db9a61f4 100644 --- a/packages/shared/src/graphql/quests.ts +++ b/packages/shared/src/graphql/quests.ts @@ -88,17 +88,6 @@ export interface QuestUpdateData { questUpdate: QuestUpdate; } -export interface QuestRotationUpdate { - updatedAt: Date; - type: QuestType; - periodStart: Date; - periodEnd: Date; -} - -export interface QuestRotationUpdateData { - questRotationUpdate: QuestRotationUpdate; -} - export enum ClientQuestEventType { VisitExplorePage = 'visit_explore_page', VisitDiscussionsPage = 'visit_discussions_page',