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'] },
+ ]
+ }
+ },
],
};