diff --git a/app/javascript/mastodon/features/annual_report/annual_report.stories.tsx b/app/javascript/mastodon/features/annual_report/annual_report.stories.tsx index 290671c0f13..20a3735e15b 100644 --- a/app/javascript/mastodon/features/annual_report/annual_report.stories.tsx +++ b/app/javascript/mastodon/features/annual_report/annual_report.stories.tsx @@ -2,11 +2,17 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { accountFactoryState, - // statusFactoryState + annualReportFactory, + statusFactoryState, } from '@/testing/factories'; import { AnnualReport } from '.'; +const SAMPLE_HASHTAG = { + name: 'Mastodon', + count: 14, +}; + const meta = { title: 'Components/AnnualReport', component: AnnualReport, @@ -16,108 +22,14 @@ const meta = { parameters: { state: { accounts: { - '1': accountFactoryState(), + '1': accountFactoryState({ display_name: 'Freddie Fruitbat' }), }, - // statuses: { - // '1': statusFactoryState(), - // }, - annualReport: { - state: 'available', - report: { - schema_version: 2, - share_url: '#', - account_id: '1', - year: 2025, - data: { - archetype: 'lurker', - time_series: [ - { - month: 1, - statuses: 0, - followers: 0, - following: 0, - }, - { - month: 2, - statuses: 0, - followers: 0, - following: 0, - }, - { - month: 3, - statuses: 0, - followers: 0, - following: 0, - }, - { - month: 4, - statuses: 0, - followers: 0, - following: 0, - }, - { - month: 5, - statuses: 1, - followers: 1, - following: 3, - }, - { - month: 6, - statuses: 7, - followers: 1, - following: 0, - }, - { - month: 7, - statuses: 2, - followers: 0, - following: 0, - }, - { - month: 8, - statuses: 2, - followers: 0, - following: 0, - }, - { - month: 9, - statuses: 11, - followers: 0, - following: 1, - }, - { - month: 10, - statuses: 12, - followers: 0, - following: 1, - }, - { - month: 11, - statuses: 6, - followers: 0, - following: 1, - }, - { - month: 12, - statuses: 4, - followers: 0, - following: 0, - }, - ], - top_hashtags: [ - { - name: 'Mastodon', - count: 14, - }, - ], - top_statuses: { - by_reblogs: '1', - by_replies: '1', - by_favourites: '1', - }, - }, - }, + statuses: { + '1': statusFactoryState(), }, + annualReport: annualReportFactory({ + top_hashtag: SAMPLE_HASHTAG, + }), }, }, } satisfies Meta; @@ -126,6 +38,68 @@ export default meta; type Story = StoryObj; -export const Default: Story = { +export const Standalone: Story = { + args: { + context: 'standalone', + }, + render: (args) => , +}; + +export const InModal: Story = { + args: { + context: 'modal', + }, + render: (args) => , +}; + +export const ArchetypeOracle: Story = { + ...InModal, + parameters: { + state: { + annualReport: annualReportFactory({ + archetype: 'oracle', + top_hashtag: SAMPLE_HASHTAG, + }), + }, + }, + render: (args) => , +}; + +export const NoHashtag: Story = { + ...InModal, + parameters: { + state: { + annualReport: annualReportFactory({ + archetype: 'booster', + }), + }, + }, + render: (args) => , +}; + +export const NoNewPosts: Story = { + ...InModal, + parameters: { + state: { + annualReport: annualReportFactory({ + archetype: 'pollster', + top_hashtag: SAMPLE_HASHTAG, + without_posts: true, + }), + }, + }, + render: (args) => , +}; + +export const NoNewPostsNoHashtag: Story = { + ...InModal, + parameters: { + state: { + annualReport: annualReportFactory({ + archetype: 'replier', + without_posts: true, + }), + }, + }, render: (args) => , }; diff --git a/app/javascript/mastodon/features/annual_report/archetype.tsx b/app/javascript/mastodon/features/annual_report/archetype.tsx index 2163a19eeb5..660a1cf29dc 100644 --- a/app/javascript/mastodon/features/annual_report/archetype.tsx +++ b/app/javascript/mastodon/features/annual_report/archetype.tsx @@ -12,7 +12,6 @@ import replier from '@/images/archetypes/replier.png'; import space_elements from '@/images/archetypes/space_elements.png'; import { Avatar } from '@/mastodon/components/avatar'; import { Button } from '@/mastodon/components/button'; -import { me } from '@/mastodon/initial_state'; import type { Account } from '@/mastodon/models/account'; import type { AnnualReport, @@ -112,11 +111,11 @@ const illustrations = { export const Archetype: React.FC<{ report: AnnualReport; account?: Account; - canShare: boolean; -}> = ({ report, account, canShare }) => { + context: 'modal' | 'standalone'; +}> = ({ report, account, context }) => { const intl = useIntl(); const wrapperRef = useRef(null); - const isSelfView = account?.id === me; + const isSelfView = context === 'modal'; const [isRevealed, setIsRevealed] = useState(!isSelfView); const reveal = useCallback(() => { @@ -209,7 +208,7 @@ export const Archetype: React.FC<{ /> )} - {isRevealed && canShare && } + {isRevealed && isSelfView && } ); }; diff --git a/app/javascript/mastodon/features/annual_report/highlighted_post.tsx b/app/javascript/mastodon/features/annual_report/highlighted_post.tsx index 5ce49476092..2ff8597aa24 100644 --- a/app/javascript/mastodon/features/annual_report/highlighted_post.tsx +++ b/app/javascript/mastodon/features/annual_report/highlighted_post.tsx @@ -19,7 +19,8 @@ const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any; export const HighlightedPost: React.FC<{ data: TopStatuses; -}> = ({ data }) => { + context: 'modal' | 'standalone'; +}> = ({ data, context }) => { const { by_reblogs, by_favourites, by_replies } = data; const statusId = by_reblogs || by_favourites || by_replies; @@ -68,10 +69,10 @@ export const HighlightedPost: React.FC<{ defaultMessage='Most popular post' /> -

{label}

+ {context === 'modal' &&

{label}

} - + ); }; diff --git a/app/javascript/mastodon/features/annual_report/index.tsx b/app/javascript/mastodon/features/annual_report/index.tsx index 2048b39670c..15266d7e264 100644 --- a/app/javascript/mastodon/features/annual_report/index.tsx +++ b/app/javascript/mastodon/features/annual_report/index.tsx @@ -66,10 +66,9 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({ 0, ); - const newFollowerCount = report.data.time_series.reduce( - (sum, item) => sum + item.followers, - 0, - ); + const newFollowerCount = + context === 'modal' && + report.data.time_series.reduce((sum, item) => sum + item.followers, 0); const topHashtag = report.data.top_hashtags[0]; @@ -99,7 +98,7 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
- +
= ({ > {!!newFollowerCount && } {!!newPostCount && } - {topHashtag && } + {topHashtag && ( + + )}
- +
); diff --git a/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx b/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx index 9a0720d8ab8..5fe386bf2bc 100644 --- a/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx +++ b/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx @@ -8,7 +8,9 @@ import styles from './index.module.scss'; export const MostUsedHashtag: React.FC<{ hashtag: NameAndCount; -}> = ({ hashtag }) => { + name: string | undefined; + context: 'modal' | 'standalone'; +}> = ({ hashtag, name, context }) => { return (
#{hashtag.name}

- + {context === 'modal' ? ( + + ) : ( + name && ( + + ) + )}

); diff --git a/app/javascript/mastodon/features/annual_report/shared_page.tsx b/app/javascript/mastodon/features/annual_report/shared_page.tsx index b2e29d0dc79..9749e4d62bb 100644 --- a/app/javascript/mastodon/features/annual_report/shared_page.tsx +++ b/app/javascript/mastodon/features/annual_report/shared_page.tsx @@ -1,5 +1,7 @@ import type { FC } from 'react'; +import { FormattedMessage } from 'react-intl'; + import { IconLogo } from '@/mastodon/components/logo'; import { AnnualReport } from './index'; @@ -11,7 +13,11 @@ export const WrapstodonSharedPage: FC = () => {
- Generated with ♥ by the Mastodon team +
); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 170a5a08367..d37ee9b1ba5 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -117,6 +117,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.shared_page.footer": "Generated with {heart} by the Mastodon team", "annual_report.summary.archetype.booster.desc_public": "{name} stayed on the hunt for posts to boost, amplifying other creators with perfect aim.", "annual_report.summary.archetype.booster.desc_self": "You stayed on the hunt for posts to boost, amplifying other creators with perfect aim.", "annual_report.summary.archetype.booster.name": "The Archer", @@ -146,6 +147,7 @@ "annual_report.summary.most_used_app.most_used_app": "most used app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "most used hashtag", "annual_report.summary.most_used_hashtag.used_count": "You included this hashtag in {count, plural, one {one post} other {# posts}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} included this hashtag in {count, plural, one {one post} other {# posts}}.", "annual_report.summary.new_posts.new_posts": "new posts", "annual_report.summary.percentile.text": "That puts you in the topof {domain} users.", "annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.", diff --git a/app/javascript/mastodon/models/annual_report.ts b/app/javascript/mastodon/models/annual_report.ts index 712f4063109..863debfc1f3 100644 --- a/app/javascript/mastodon/models/annual_report.ts +++ b/app/javascript/mastodon/models/annual_report.ts @@ -16,9 +16,9 @@ export interface TimeSeriesMonth { } export interface TopStatuses { - by_reblogs: number; - by_favourites: number; - by_replies: number; + by_reblogs: string; + by_favourites: string; + by_replies: string; } export type Archetype = diff --git a/app/javascript/testing/factories.ts b/app/javascript/testing/factories.ts index f86aa772dc8..95afb41028a 100644 --- a/app/javascript/testing/factories.ts +++ b/app/javascript/testing/factories.ts @@ -1,4 +1,4 @@ -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, List } from 'immutable'; import type { ApiRelationshipJSON } from '@/mastodon/api_types/relationships'; import type { ApiStatusJSON } from '@/mastodon/api_types/statuses'; @@ -7,6 +7,7 @@ import type { UnicodeEmojiData, } from '@/mastodon/features/emoji/types'; import { createAccountFromServerJSON } from '@/mastodon/models/account'; +import type { AnnualReport } from '@/mastodon/models/annual_report'; import type { Status } from '@/mastodon/models/status'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; @@ -75,16 +76,18 @@ export const statusFactory: FactoryFunction = ({ mentions: [], tags: [], emojis: [], - content: '

This is a test status.

', + contentHtml: '

This is a test status.

', ...data, }); export const statusFactoryState = ( options: FactoryOptions = {}, ) => - ImmutableMap( - statusFactory(options) as unknown as Record, - ) as unknown as Status; + ImmutableMap({ + ...(statusFactory(options) as unknown as Record), + account: options.account?.id ?? '1', + tags: List(options.tags), + }) as unknown as Status; export const relationshipsFactory: FactoryFunction = ({ id, @@ -130,3 +133,119 @@ export function customEmojiFactory( ...data, }; } + +interface AnnualReportState { + state: 'available'; + report: AnnualReport; +} + +interface AnnualReportFactoryOptions { + account_id?: string; + status_id?: string; + archetype?: AnnualReport['data']['archetype']; + year?: number; + top_hashtag?: AnnualReport['data']['top_hashtags'][0]; + without_posts?: boolean; +} + +export function annualReportFactory({ + account_id = '1', + status_id = '1', + archetype = 'lurker', + year, + top_hashtag, + without_posts = false, +}: AnnualReportFactoryOptions = {}): AnnualReportState { + return { + state: 'available', + report: { + schema_version: 2, + share_url: '#', + account_id, + year: year ?? 2025, + data: { + archetype, + time_series: [ + { + month: 1, + statuses: 0, + followers: 0, + following: 0, + }, + { + month: 2, + statuses: 0, + followers: 0, + following: 0, + }, + { + month: 3, + statuses: 0, + followers: 0, + following: 0, + }, + { + month: 4, + statuses: 0, + followers: 0, + following: 0, + }, + { + month: 5, + statuses: without_posts ? 0 : 1, + followers: 1, + following: 3, + }, + { + month: 6, + statuses: without_posts ? 0 : 7, + followers: 1, + following: 0, + }, + { + month: 7, + statuses: without_posts ? 0 : 2, + followers: 0, + following: 0, + }, + { + month: 8, + statuses: without_posts ? 0 : 2, + followers: 0, + following: 0, + }, + { + month: 9, + statuses: without_posts ? 0 : 11, + followers: 0, + following: 1, + }, + { + month: 10, + statuses: without_posts ? 0 : 12, + followers: 0, + following: 1, + }, + { + month: 11, + statuses: without_posts ? 0 : 6, + followers: 0, + following: 1, + }, + { + month: 12, + statuses: without_posts ? 0 : 4, + followers: 0, + following: 0, + }, + ], + top_hashtags: top_hashtag ? [top_hashtag] : [], + top_statuses: { + by_reblogs: status_id, + by_replies: status_id, + by_favourites: status_id, + }, + }, + }, + }; +}