Fix Wrapstodon Storybook & other Wrapstodon issues (#37189)

This commit is contained in:
diondiondion 2025-12-10 15:07:25 +01:00 committed by GitHub
parent 37426288d9
commit 8137ce87ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 250 additions and 136 deletions

View File

@ -2,11 +2,17 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import { import {
accountFactoryState, accountFactoryState,
// statusFactoryState annualReportFactory,
statusFactoryState,
} from '@/testing/factories'; } from '@/testing/factories';
import { AnnualReport } from '.'; import { AnnualReport } from '.';
const SAMPLE_HASHTAG = {
name: 'Mastodon',
count: 14,
};
const meta = { const meta = {
title: 'Components/AnnualReport', title: 'Components/AnnualReport',
component: AnnualReport, component: AnnualReport,
@ -16,108 +22,14 @@ const meta = {
parameters: { parameters: {
state: { state: {
accounts: { 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<typeof AnnualReport>; } satisfies Meta<typeof AnnualReport>;
@ -126,6 +38,68 @@ export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const Default: Story = { export const Standalone: Story = {
args: {
context: 'standalone',
},
render: (args) => <AnnualReport {...args} />,
};
export const InModal: Story = {
args: {
context: 'modal',
},
render: (args) => <AnnualReport {...args} />,
};
export const ArchetypeOracle: Story = {
...InModal,
parameters: {
state: {
annualReport: annualReportFactory({
archetype: 'oracle',
top_hashtag: SAMPLE_HASHTAG,
}),
},
},
render: (args) => <AnnualReport {...args} />,
};
export const NoHashtag: Story = {
...InModal,
parameters: {
state: {
annualReport: annualReportFactory({
archetype: 'booster',
}),
},
},
render: (args) => <AnnualReport {...args} />,
};
export const NoNewPosts: Story = {
...InModal,
parameters: {
state: {
annualReport: annualReportFactory({
archetype: 'pollster',
top_hashtag: SAMPLE_HASHTAG,
without_posts: true,
}),
},
},
render: (args) => <AnnualReport {...args} />,
};
export const NoNewPostsNoHashtag: Story = {
...InModal,
parameters: {
state: {
annualReport: annualReportFactory({
archetype: 'replier',
without_posts: true,
}),
},
},
render: (args) => <AnnualReport {...args} />, render: (args) => <AnnualReport {...args} />,
}; };

View File

@ -12,7 +12,6 @@ import replier from '@/images/archetypes/replier.png';
import space_elements from '@/images/archetypes/space_elements.png'; import space_elements from '@/images/archetypes/space_elements.png';
import { Avatar } from '@/mastodon/components/avatar'; import { Avatar } from '@/mastodon/components/avatar';
import { Button } from '@/mastodon/components/button'; import { Button } from '@/mastodon/components/button';
import { me } from '@/mastodon/initial_state';
import type { Account } from '@/mastodon/models/account'; import type { Account } from '@/mastodon/models/account';
import type { import type {
AnnualReport, AnnualReport,
@ -112,11 +111,11 @@ const illustrations = {
export const Archetype: React.FC<{ export const Archetype: React.FC<{
report: AnnualReport; report: AnnualReport;
account?: Account; account?: Account;
canShare: boolean; context: 'modal' | 'standalone';
}> = ({ report, account, canShare }) => { }> = ({ report, account, context }) => {
const intl = useIntl(); const intl = useIntl();
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const isSelfView = account?.id === me; const isSelfView = context === 'modal';
const [isRevealed, setIsRevealed] = useState(!isSelfView); const [isRevealed, setIsRevealed] = useState(!isSelfView);
const reveal = useCallback(() => { const reveal = useCallback(() => {
@ -209,7 +208,7 @@ export const Archetype: React.FC<{
/> />
</Button> </Button>
)} )}
{isRevealed && canShare && <ShareButton report={report} />} {isRevealed && isSelfView && <ShareButton report={report} />}
</div> </div>
); );
}; };

View File

@ -19,7 +19,8 @@ const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
export const HighlightedPost: React.FC<{ export const HighlightedPost: React.FC<{
data: TopStatuses; data: TopStatuses;
}> = ({ data }) => { context: 'modal' | 'standalone';
}> = ({ data, context }) => {
const { by_reblogs, by_favourites, by_replies } = data; const { by_reblogs, by_favourites, by_replies } = data;
const statusId = by_reblogs || by_favourites || by_replies; const statusId = by_reblogs || by_favourites || by_replies;
@ -68,10 +69,10 @@ export const HighlightedPost: React.FC<{
defaultMessage='Most popular post' defaultMessage='Most popular post'
/> />
</h2> </h2>
<p>{label}</p> {context === 'modal' && <p>{label}</p>}
</div> </div>
<StatusQuoteManager showActions={false} id={`${statusId}`} /> <StatusQuoteManager showActions={false} id={statusId} />
</div> </div>
); );
}; };

View File

@ -66,10 +66,9 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
0, 0,
); );
const newFollowerCount = report.data.time_series.reduce( const newFollowerCount =
(sum, item) => sum + item.followers, context === 'modal' &&
0, report.data.time_series.reduce((sum, item) => sum + item.followers, 0);
);
const topHashtag = report.data.top_hashtags[0]; const topHashtag = report.data.top_hashtags[0];
@ -99,7 +98,7 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
</div> </div>
<div className={styles.stack}> <div className={styles.stack}>
<HighlightedPost data={report.data.top_statuses} /> <HighlightedPost data={report.data.top_statuses} context={context} />
<div <div
className={moduleClassNames(styles.statsGrid, { className={moduleClassNames(styles.statsGrid, {
noHashtag: !topHashtag, noHashtag: !topHashtag,
@ -109,13 +108,15 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
> >
{!!newFollowerCount && <Followers count={newFollowerCount} />} {!!newFollowerCount && <Followers count={newFollowerCount} />}
{!!newPostCount && <NewPosts count={newPostCount} />} {!!newPostCount && <NewPosts count={newPostCount} />}
{topHashtag && <MostUsedHashtag hashtag={topHashtag} />} {topHashtag && (
</div> <MostUsedHashtag
<Archetype hashtag={topHashtag}
report={report} name={account?.display_name}
account={account} context={context}
canShare={context === 'modal'}
/> />
)}
</div>
<Archetype report={report} account={account} context={context} />
</div> </div>
</div> </div>
); );

View File

@ -8,7 +8,9 @@ import styles from './index.module.scss';
export const MostUsedHashtag: React.FC<{ export const MostUsedHashtag: React.FC<{
hashtag: NameAndCount; hashtag: NameAndCount;
}> = ({ hashtag }) => { name: string | undefined;
context: 'modal' | 'standalone';
}> = ({ hashtag, name, context }) => {
return ( return (
<div <div
className={classNames(styles.box, styles.mostUsedHashtag, styles.content)} className={classNames(styles.box, styles.mostUsedHashtag, styles.content)}
@ -23,11 +25,21 @@ export const MostUsedHashtag: React.FC<{
<div className={styles.statExtraLarge}>#{hashtag.name}</div> <div className={styles.statExtraLarge}>#{hashtag.name}</div>
<p> <p>
{context === 'modal' ? (
<FormattedMessage <FormattedMessage
id='annual_report.summary.most_used_hashtag.used_count' id='annual_report.summary.most_used_hashtag.used_count'
defaultMessage='You included this hashtag in {count, plural, one {one post} other {# posts}}.' defaultMessage='You included this hashtag in {count, plural, one {one post} other {# posts}}.'
values={{ count: hashtag.count }} values={{ count: hashtag.count }}
/> />
) : (
name && (
<FormattedMessage
id='annual_report.summary.most_used_hashtag.used_count_public'
defaultMessage='{name} included this hashtag in {count, plural, one {one post} other {# posts}}.'
values={{ count: hashtag.count, name }}
/>
)
)}
</p> </p>
</div> </div>
); );

View File

@ -1,5 +1,7 @@
import type { FC } from 'react'; import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import { IconLogo } from '@/mastodon/components/logo'; import { IconLogo } from '@/mastodon/components/logo';
import { AnnualReport } from './index'; import { AnnualReport } from './index';
@ -11,7 +13,11 @@ export const WrapstodonSharedPage: FC = () => {
<AnnualReport /> <AnnualReport />
<footer className={classes.footer}> <footer className={classes.footer}>
<IconLogo className={classes.logo} /> <IconLogo className={classes.logo} />
Generated with by the Mastodon team <FormattedMessage
id='annual_report.shared_page.footer'
defaultMessage='Generated with {heart} by the Mastodon team'
values={{ heart: '♥' }}
/>
</footer> </footer>
</main> </main>
); );

View File

@ -117,6 +117,7 @@
"annual_report.announcement.action_view": "View my Wrapstodon", "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.description": "Discover more about your engagement on Mastodon over the past year.",
"annual_report.announcement.title": "Wrapstodon {year} has arrived", "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_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.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", "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_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.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": "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.new_posts.new_posts": "new posts",
"annual_report.summary.percentile.text": "<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of {domain} users.</bottomLabel>", "annual_report.summary.percentile.text": "<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of {domain} users.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.", "annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.",

View File

@ -16,9 +16,9 @@ export interface TimeSeriesMonth {
} }
export interface TopStatuses { export interface TopStatuses {
by_reblogs: number; by_reblogs: string;
by_favourites: number; by_favourites: string;
by_replies: number; by_replies: string;
} }
export type Archetype = export type Archetype =

View File

@ -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 { ApiRelationshipJSON } from '@/mastodon/api_types/relationships';
import type { ApiStatusJSON } from '@/mastodon/api_types/statuses'; import type { ApiStatusJSON } from '@/mastodon/api_types/statuses';
@ -7,6 +7,7 @@ import type {
UnicodeEmojiData, UnicodeEmojiData,
} from '@/mastodon/features/emoji/types'; } from '@/mastodon/features/emoji/types';
import { createAccountFromServerJSON } from '@/mastodon/models/account'; import { createAccountFromServerJSON } from '@/mastodon/models/account';
import type { AnnualReport } from '@/mastodon/models/annual_report';
import type { Status } from '@/mastodon/models/status'; import type { Status } from '@/mastodon/models/status';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
@ -75,16 +76,18 @@ export const statusFactory: FactoryFunction<ApiStatusJSON> = ({
mentions: [], mentions: [],
tags: [], tags: [],
emojis: [], emojis: [],
content: '<p>This is a test status.</p>', contentHtml: '<p>This is a test status.</p>',
...data, ...data,
}); });
export const statusFactoryState = ( export const statusFactoryState = (
options: FactoryOptions<ApiStatusJSON> = {}, options: FactoryOptions<ApiStatusJSON> = {},
) => ) =>
ImmutableMap<string, unknown>( ImmutableMap<string, unknown>({
statusFactory(options) as unknown as Record<string, unknown>, ...(statusFactory(options) as unknown as Record<string, unknown>),
) as unknown as Status; account: options.account?.id ?? '1',
tags: List(options.tags),
}) as unknown as Status;
export const relationshipsFactory: FactoryFunction<ApiRelationshipJSON> = ({ export const relationshipsFactory: FactoryFunction<ApiRelationshipJSON> = ({
id, id,
@ -130,3 +133,119 @@ export function customEmojiFactory(
...data, ...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,
},
},
},
};
}