From e206b0d0dead4cf48e211f9e57724ae63d3d8b08 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 12 Dec 2025 11:11:47 +0100 Subject: [PATCH] Wrapstodon: Add nav modal (#37210) --- app/javascript/images/icons/icon_planet.svg | 3 + .../annual_report/announcement/index.tsx | 2 +- .../announcement/styles.module.scss | 5 ++ .../features/annual_report/index.module.scss | 4 ++ .../mastodon/features/annual_report/index.tsx | 29 ++++++---- .../mastodon/features/annual_report/modal.tsx | 56 ++++++++++++++++--- .../features/annual_report/nav_item.tsx | 55 ++++++++++++++++++ .../features/navigation_panel/index.tsx | 4 ++ app/javascript/mastodon/locales/en.json | 1 + stylelint.config.js | 11 +++- 10 files changed, 150 insertions(+), 20 deletions(-) create mode 100644 app/javascript/images/icons/icon_planet.svg create mode 100644 app/javascript/mastodon/features/annual_report/nav_item.tsx diff --git a/app/javascript/images/icons/icon_planet.svg b/app/javascript/images/icons/icon_planet.svg new file mode 100644 index 0000000000..e8bf34c7a6 --- /dev/null +++ b/app/javascript/images/icons/icon_planet.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/javascript/mastodon/features/annual_report/announcement/index.tsx b/app/javascript/mastodon/features/annual_report/announcement/index.tsx index ee73d35352..283e95f594 100644 --- a/app/javascript/mastodon/features/annual_report/announcement/index.tsx +++ b/app/javascript/mastodon/features/annual_report/announcement/index.tsx @@ -11,7 +11,7 @@ export interface AnnualReportAnnouncementProps { year: string; state: Exclude; onRequestBuild: () => void; - onOpen: () => void; + onOpen?: () => void; // This is optional when inside the modal, as it won't be shown then. onDismiss: () => void; } diff --git a/app/javascript/mastodon/features/annual_report/announcement/styles.module.scss b/app/javascript/mastodon/features/annual_report/announcement/styles.module.scss index b96ca2e679..9ec62fa0fd 100644 --- a/app/javascript/mastodon/features/annual_report/announcement/styles.module.scss +++ b/app/javascript/mastodon/features/annual_report/announcement/styles.module.scss @@ -16,6 +16,7 @@ var(--color-bg-primary); border-bottom: 1px solid var(--color-border-primary); position: relative; + pointer-events: all; h2 { font-size: 20px; @@ -34,4 +35,8 @@ right: 8px; margin-inline: 0; } + + :global(.modal-root__modal) & { + border-radius: 16px; + } } diff --git a/app/javascript/mastodon/features/annual_report/index.module.scss b/app/javascript/mastodon/features/annual_report/index.module.scss index 9e9a6464c1..375b2e211e 100644 --- a/app/javascript/mastodon/features/annual_report/index.module.scss +++ b/app/javascript/mastodon/features/annual_report/index.module.scss @@ -332,3 +332,7 @@ $mobile-breakpoint: 540px; left: 0; mix-blend-mode: screen; } + +.navItemBadge { + background: var(--color-bg-brand-soft); +} diff --git a/app/javascript/mastodon/features/annual_report/index.tsx b/app/javascript/mastodon/features/annual_report/index.tsx index 7995caae87..df4ad47085 100644 --- a/app/javascript/mastodon/features/annual_report/index.tsx +++ b/app/javascript/mastodon/features/annual_report/index.tsx @@ -11,7 +11,11 @@ import { closeModal } from '@/mastodon/actions/modal'; import { IconButton } from '@/mastodon/components/icon_button'; import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; import { me } from '@/mastodon/initial_state'; -import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import { + createAppSelector, + useAppDispatch, + useAppSelector, +} from '@/mastodon/store'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import { Archetype } from './archetype'; @@ -23,21 +27,26 @@ import { NewPosts } from './new_posts'; const moduleClassNames = classNames.bind(styles); +const accountSelector = createAppSelector( + [(state) => state.accounts, (state) => state.annualReport.report], + (accounts, report) => { + if (me) { + return accounts.get(me); + } + if (report?.schema_version === 2) { + return accounts.get(report.account_id); + } + return undefined; + }, +); + export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({ context = 'standalone', }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const report = useAppSelector((state) => state.annualReport.report); - const account = useAppSelector((state) => { - if (me) { - return state.accounts.get(me); - } - if (report?.schema_version === 2) { - return state.accounts.get(report.account_id); - } - return undefined; - }); + const account = useAppSelector(accountSelector); const close = useCallback(() => { dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false })); diff --git a/app/javascript/mastodon/features/annual_report/modal.tsx b/app/javascript/mastodon/features/annual_report/modal.tsx index c67a6ca19c..953732cba5 100644 --- a/app/javascript/mastodon/features/annual_report/modal.tsx +++ b/app/javascript/mastodon/features/annual_report/modal.tsx @@ -1,11 +1,17 @@ +import type { MouseEventHandler } from 'react'; import { useCallback, useEffect } from 'react'; import classNames from 'classnames'; import { closeModal } from '@/mastodon/actions/modal'; -import { useAppDispatch } from '@/mastodon/store'; +import { + generateReport, + selectWrapstodonYear, +} from '@/mastodon/reducers/slices/annual_report'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; import { AnnualReport } from '.'; +import { AnnualReportAnnouncement } from './announcement'; import styles from './index.module.scss'; const AnnualReportModal: React.FC<{ @@ -15,17 +21,42 @@ const AnnualReportModal: React.FC<{ onChangeBackgroundColor('var(--color-bg-media-base)'); }, [onChangeBackgroundColor]); + const { state } = useAppSelector((state) => state.annualReport); + const year = useAppSelector(selectWrapstodonYear); + + const showAnnouncement = year && state && state !== 'available'; + const dispatch = useAppDispatch(); - const handleCloseModal = useCallback>( + + const handleBuildRequest = useCallback(() => { + void dispatch(generateReport()); + }, [dispatch]); + + const handleClose = useCallback(() => { + dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false })); + }, [dispatch]); + + const handleCloseModal: MouseEventHandler = useCallback( (e) => { - if (e.target === e.currentTarget) - dispatch( - closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }), - ); + if (e.target === e.currentTarget) { + handleClose(); + } }, - [dispatch], + [handleClose], ); + // Auto-close if ineligible + useEffect(() => { + if (state === 'ineligible') { + handleClose(); + } + }, [handleClose, state]); + + if (state === 'ineligible') { + // Not sure how you got here, but don't show anything. + return null; + } + return ( // It's fine not to provide a keyboard handler here since there is a global // [Esc] key listener that will close open modals. @@ -40,7 +71,16 @@ const AnnualReportModal: React.FC<{ )} onClick={handleCloseModal} > - + {!showAnnouncement ? ( + + ) : ( + + )} ); }; diff --git a/app/javascript/mastodon/features/annual_report/nav_item.tsx b/app/javascript/mastodon/features/annual_report/nav_item.tsx new file mode 100644 index 0000000000..bc293a7947 --- /dev/null +++ b/app/javascript/mastodon/features/annual_report/nav_item.tsx @@ -0,0 +1,55 @@ +import { useCallback } from 'react'; +import type { FC } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +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, + useAppSelector, +} from '@/mastodon/store'; + +import classes from './index.module.scss'; + +const selectReportModalOpen = createAppSelector( + [(state) => state.modal.getIn(['stack', 0, 'modalType'])], + (modalType) => modalType === 'ANNUAL_REPORT', +); + +export const AnnualReportNavItem: FC = () => { + const { state } = useAppSelector((state) => state.annualReport); + const year = useAppSelector(selectWrapstodonYear); + const active = useAppSelector(selectReportModalOpen); + + const dispatch = useAppDispatch(); + const handleClick = useCallback(() => { + dispatch(openModal({ modalType: 'ANNUAL_REPORT', modalProps: {} })); + }, [dispatch]); + + if (!year || !state || state === 'ineligible') { + return null; + } + + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/navigation_panel/index.tsx b/app/javascript/mastodon/features/navigation_panel/index.tsx index 5b5af7a4e5..0dbe94cc21 100644 --- a/app/javascript/mastodon/features/navigation_panel/index.tsx +++ b/app/javascript/mastodon/features/navigation_panel/index.tsx @@ -46,6 +46,8 @@ import { canViewFeed } from 'mastodon/permissions'; import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; +import { AnnualReportNavItem } from '../annual_report/nav_item'; + import { DisabledAccountBanner } from './components/disabled_account_banner'; import { FollowedTagsPanel } from './components/followed_tags_panel'; import { ListPanel } from './components/list_panel'; @@ -294,6 +296,8 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({ + +
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index e91af0c904..b4ea6dbeb9 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -118,6 +118,7 @@ "annual_report.announcement.action_view": "View my Wrapstodon", "annual_report.announcement.description": "Discover more about your engagement on Mastodon over the past year.", "annual_report.announcement.title": "Wrapstodon {year} has arrived", + "annual_report.nav_item.badge": "New", "annual_report.shared_page.donate": "Donate", "annual_report.shared_page.footer": "Generated with {heart} by the Mastodon team", "annual_report.shared_page.sign_up": "Sign up", diff --git a/stylelint.config.js b/stylelint.config.js index b1f34b7f19..9ccee47748 100644 --- a/stylelint.config.js +++ b/stylelint.config.js @@ -31,7 +31,7 @@ module.exports = { }, overrides: [ { - 'files': ['app/javascript/styles/entrypoints/mailer.scss'], + files: ['app/javascript/styles/entrypoints/mailer.scss'], rules: { 'property-no-unknown': [ true, @@ -42,5 +42,14 @@ module.exports = { ], }, }, + { + files: ['app/javascript/**/*.module.scss'], + rules: { + 'selector-pseudo-class-no-unknown': [ + true, + { ignorePseudoClasses: ['global'] }, + ] + } + }, ], };