From a9c84529b2c1c2ef3cf5c88521917834f2fe9e1c Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 16 Dec 2025 14:00:40 +0100 Subject: [PATCH] Wrapstodon: Load report data only on display (#37269) --- app/javascript/mastodon/actions/server.js | 5 ---- .../mastodon/features/annual_report/index.tsx | 10 ++++++- .../mastodon/features/annual_report/modal.tsx | 8 ++---- .../features/annual_report/nav_item.tsx | 4 +-- .../features/annual_report/timeline.tsx | 8 ++---- app/javascript/mastodon/features/ui/index.jsx | 2 ++ app/javascript/mastodon/initial_state.ts | 11 ++++---- .../mastodon/reducers/slices/annual_report.ts | 27 ++++++++----------- 8 files changed, 33 insertions(+), 42 deletions(-) diff --git a/app/javascript/mastodon/actions/server.js b/app/javascript/mastodon/actions/server.js index 47b6e7a176..32ee093afa 100644 --- a/app/javascript/mastodon/actions/server.js +++ b/app/javascript/mastodon/actions/server.js @@ -1,5 +1,3 @@ -import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report'; - import api from '../api'; import { importFetchedAccount } from './importer'; @@ -31,9 +29,6 @@ export const fetchServer = () => (dispatch, getState) => { .get('/api/v2/instance').then(({ data }) => { if (data.contact.account) dispatch(importFetchedAccount(data.contact.account)); dispatch(fetchServerSuccess(data)); - if (data.wrapstodon) { - void dispatch(checkAnnualReport()); - } }).catch(err => dispatch(fetchServerFail(err))); }; diff --git a/app/javascript/mastodon/features/annual_report/index.tsx b/app/javascript/mastodon/features/annual_report/index.tsx index fe55d250c6..218d60d26d 100644 --- a/app/javascript/mastodon/features/annual_report/index.tsx +++ b/app/javascript/mastodon/features/annual_report/index.tsx @@ -10,6 +10,7 @@ import classNames from 'classnames/bind'; import { closeModal } from '@/mastodon/actions/modal'; import { IconButton } from '@/mastodon/components/icon_button'; import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; +import { getReport } from '@/mastodon/reducers/slices/annual_report'; import { createAppSelector, useAppDispatch, @@ -43,6 +44,13 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({ const dispatch = useAppDispatch(); const report = useAppSelector((state) => state.annualReport.report); const account = useAppSelector(accountSelector); + const needsReport = !report; // Make into boolean to avoid object comparison in deps. + + useEffect(() => { + if (needsReport) { + void dispatch(getReport()); + } + }, [dispatch, needsReport]); const close = useCallback(() => { dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false })); @@ -57,7 +65,7 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({ } }, [pathname, initialPathname, close]); - if (!report) { + if (needsReport) { return ; } diff --git a/app/javascript/mastodon/features/annual_report/modal.tsx b/app/javascript/mastodon/features/annual_report/modal.tsx index 953732cba5..01d7c4bbdb 100644 --- a/app/javascript/mastodon/features/annual_report/modal.tsx +++ b/app/javascript/mastodon/features/annual_report/modal.tsx @@ -4,10 +4,7 @@ import { useCallback, useEffect } from 'react'; import classNames from 'classnames'; import { closeModal } from '@/mastodon/actions/modal'; -import { - generateReport, - selectWrapstodonYear, -} from '@/mastodon/reducers/slices/annual_report'; +import { generateReport } from '@/mastodon/reducers/slices/annual_report'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import { AnnualReport } from '.'; @@ -21,8 +18,7 @@ const AnnualReportModal: React.FC<{ onChangeBackgroundColor('var(--color-bg-media-base)'); }, [onChangeBackgroundColor]); - const { state } = useAppSelector((state) => state.annualReport); - const year = useAppSelector(selectWrapstodonYear); + const { state, year } = useAppSelector((state) => state.annualReport); const showAnnouncement = year && state && state !== 'available'; diff --git a/app/javascript/mastodon/features/annual_report/nav_item.tsx b/app/javascript/mastodon/features/annual_report/nav_item.tsx index bc293a7947..435e2b1f70 100644 --- a/app/javascript/mastodon/features/annual_report/nav_item.tsx +++ b/app/javascript/mastodon/features/annual_report/nav_item.tsx @@ -8,7 +8,6 @@ import classNames from 'classnames'; import IconPlanet from '@/images/icons/icon_planet.svg?react'; import { openModal } from '@/mastodon/actions/modal'; import { Icon } from '@/mastodon/components/icon'; -import { selectWrapstodonYear } from '@/mastodon/reducers/slices/annual_report'; import { createAppSelector, useAppDispatch, @@ -23,8 +22,7 @@ const selectReportModalOpen = createAppSelector( ); export const AnnualReportNavItem: FC = () => { - const { state } = useAppSelector((state) => state.annualReport); - const year = useAppSelector(selectWrapstodonYear); + const { state, year } = useAppSelector((state) => state.annualReport); const active = useAppSelector(selectReportModalOpen); const dispatch = useAppDispatch(); diff --git a/app/javascript/mastodon/features/annual_report/timeline.tsx b/app/javascript/mastodon/features/annual_report/timeline.tsx index 4280c2a98a..28a4b1d273 100644 --- a/app/javascript/mastodon/features/annual_report/timeline.tsx +++ b/app/javascript/mastodon/features/annual_report/timeline.tsx @@ -3,17 +3,13 @@ import type { FC } from 'react'; import { openModal } from '@/mastodon/actions/modal'; import { useDismissible } from '@/mastodon/hooks/useDismissible'; -import { - generateReport, - selectWrapstodonYear, -} from '@/mastodon/reducers/slices/annual_report'; +import { generateReport } from '@/mastodon/reducers/slices/annual_report'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import { AnnualReportAnnouncement } from './announcement'; export const AnnualReportTimeline: FC = () => { - const { state } = useAppSelector((state) => state.annualReport); - const year = useAppSelector(selectWrapstodonYear); + const { state, year } = useAppSelector((state) => state.annualReport); const dispatch = useAppDispatch(); const handleBuildRequest = useCallback(() => { diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 0d73106808..61317fff5b 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -21,6 +21,7 @@ import { PictureInPicture } from 'mastodon/features/picture_in_picture'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { layoutFromWindow } from 'mastodon/is_mobile'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; +import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { clearHeight } from '../../actions/height_cache'; @@ -396,6 +397,7 @@ class UI extends PureComponent { this.props.dispatch(expandHomeTimeline()); this.props.dispatch(fetchNotifications()); this.props.dispatch(fetchServerTranslationLanguages()); + this.props.dispatch(checkAnnualReport()); setTimeout(() => this.props.dispatch(fetchServer()), 3000); } diff --git a/app/javascript/mastodon/initial_state.ts b/app/javascript/mastodon/initial_state.ts index 358c307c30..9af0c26f93 100644 --- a/app/javascript/mastodon/initial_state.ts +++ b/app/javascript/mastodon/initial_state.ts @@ -1,12 +1,8 @@ +import type { ApiAnnualReportState } from './api/annual_report'; import type { ApiAccountJSON } from './api_types/accounts'; type InitialStateLanguage = [code: string, name: string, localName: string]; -interface InitialWrapstodonState { - year: number; - state: 'available' | 'generating' | 'eligible' | 'ineligible'; -} - interface InitialStateMeta { access_token: string; advanced_layout?: boolean; @@ -63,6 +59,11 @@ interface Role { highlighted: boolean; } +interface InitialWrapstodonState { + year: number; + state: ApiAnnualReportState; +} + export interface InitialState { accounts: Record; languages: InitialStateLanguage[]; diff --git a/app/javascript/mastodon/reducers/slices/annual_report.ts b/app/javascript/mastodon/reducers/slices/annual_report.ts index 5e7f44798a..798fe7cc9b 100644 --- a/app/javascript/mastodon/reducers/slices/annual_report.ts +++ b/app/javascript/mastodon/reducers/slices/annual_report.ts @@ -13,12 +13,10 @@ import { } from '@/mastodon/api/annual_report'; import { wrapstodon } from '@/mastodon/initial_state'; import type { AnnualReport } from '@/mastodon/models/annual_report'; - import { - createAppSelector, createAppThunk, createDataLoadingThunk, -} from '../../store/typed_functions'; +} from '@/mastodon/store/typed_functions'; interface AnnualReportState { year?: number; @@ -57,18 +55,17 @@ const annualReportSlice = createSlice({ export const annualReport = annualReportSlice.reducer; export const { setReport } = annualReportSlice.actions; -export const selectWrapstodonYear = createAppSelector( - [(state) => state.annualReport.year], - (year: number | null | undefined) => year ?? null, -); - -// This kicks everything off, and is called after fetching the server info. +// Called on initial load to check if we need to refresh the report state. export const checkAnnualReport = createAppThunk( `${annualReportSlice.name}/checkAnnualReport`, (_arg: unknown, { dispatch, getState }) => { - const year = selectWrapstodonYear(getState()); + const { state, year } = getState().annualReport; const me = getState().meta.get('me') as string; - if (!year || !me) { + + // If we have a state, we only need to fetch it again to poll for changes. + const needsStateRefresh = !state || state === 'generating'; + + if (!year || !me || !needsStateRefresh) { return; } void dispatch(fetchReportState()); @@ -78,7 +75,7 @@ export const checkAnnualReport = createAppThunk( const fetchReportState = createDataLoadingThunk( `${annualReportSlice.name}/fetchReportState`, async (_arg: unknown, { getState }) => { - const year = selectWrapstodonYear(getState()); + const { year } = getState().annualReport; if (!year) { throw new Error('Year is not set'); } @@ -89,8 +86,6 @@ const fetchReportState = createDataLoadingThunk( window.setTimeout(() => { void dispatch(fetchReportState()); }, 1_000 * refresh.retry); - } else if (state === 'available') { - void dispatch(getReport()); } return state; @@ -102,7 +97,7 @@ const fetchReportState = createDataLoadingThunk( export const generateReport = createDataLoadingThunk( `${annualReportSlice.name}/generateReport`, async (_arg: unknown, { getState }) => { - const year = selectWrapstodonYear(getState()); + const { year } = getState().annualReport; if (!year) { throw new Error('Year is not set'); } @@ -116,7 +111,7 @@ export const generateReport = createDataLoadingThunk( export const getReport = createDataLoadingThunk( `${annualReportSlice.name}/getReport`, async (_arg: unknown, { getState }) => { - const year = selectWrapstodonYear(getState()); + const { year } = getState().annualReport; if (!year) { throw new Error('Year is not set'); }