diff --git a/.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch b/.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch
deleted file mode 100644
index 0b3f94d09e..0000000000
--- a/.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/lib/index.js b/lib/index.js
-index 16ed6be8be8f555cc99096c2ff60954b42dc313d..d009c069770d066ad0db7ad02de1ea473a29334e 100644
---- a/lib/index.js
-+++ b/lib/index.js
-@@ -99,7 +99,7 @@ function lodash(_ref) {
-
- var node = _ref3;
-
-- if ((0, _types.isModuleDeclaration)(node)) {
-+ if ((0, _types.isImportDeclaration)(node) || (0, _types.isExportDeclaration)(node)) {
- isModule = true;
- break;
- }
diff --git a/Gemfile.lock b/Gemfile.lock
index ada28236f6..43d9cc142b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -304,8 +304,8 @@ GEM
highline (3.1.2)
reline
hiredis (0.6.3)
- hiredis-client (0.26.1)
- redis-client (= 0.26.1)
+ hiredis-client (0.26.2)
+ redis-client (= 0.26.2)
hkdf (0.3.0)
htmlentities (4.3.4)
http (5.3.1)
@@ -469,7 +469,7 @@ GEM
nokogiri (1.18.10)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
- oj (3.16.12)
+ oj (3.16.13)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.4)
@@ -703,7 +703,7 @@ GEM
reline
redcarpet (3.6.1)
redis (4.8.1)
- redis-client (0.26.1)
+ redis-client (0.26.2)
connection_pool
regexp_parser (2.11.3)
reline (0.6.3)
diff --git a/app/controllers/api/v1_alpha/collections_controller.rb b/app/controllers/api/v1_alpha/collections_controller.rb
index 65c08d8d6b..d0c4e0f3f0 100644
--- a/app/controllers/api/v1_alpha/collections_controller.rb
+++ b/app/controllers/api/v1_alpha/collections_controller.rb
@@ -71,7 +71,6 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
def set_collections
@collections = @account.collections
.with_tag
- .with_item_count
.order(created_at: :desc)
.offset(offset_param)
.limit(limit_param(DEFAULT_COLLECTIONS_LIMIT))
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 645b256a67..8d40823417 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -155,6 +155,7 @@ module ApplicationHelper
def html_classes
output = []
+ output << content_for(:html_classes)
output << 'system-font' if current_account&.user&.setting_system_font_ui
output << 'custom-scrollbars' unless current_account&.user&.setting_system_scrollbars_ui
output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')
diff --git a/app/javascript/entrypoints/wrapstodon.tsx b/app/javascript/entrypoints/wrapstodon.tsx
index e1eebcce57..7a74e18d52 100644
--- a/app/javascript/entrypoints/wrapstodon.tsx
+++ b/app/javascript/entrypoints/wrapstodon.tsx
@@ -2,13 +2,11 @@ import { createRoot } from 'react-dom/client';
import { Provider as ReduxProvider } from 'react-redux';
-import {
- importFetchedAccounts,
- importFetchedStatuses,
-} from '@/mastodon/actions/importer';
+import { importFetchedStatuses } from '@/mastodon/actions/importer';
+import { hydrateStore } from '@/mastodon/actions/store';
import type { ApiAnnualReportResponse } from '@/mastodon/api/annual_report';
import { Router } from '@/mastodon/components/router';
-import { WrapstodonShare } from '@/mastodon/features/annual_report/share';
+import { WrapstodonSharedPage } from '@/mastodon/features/annual_report/shared_page';
import { IntlProvider, loadLocale } from '@/mastodon/locales';
import { loadPolyfills } from '@/mastodon/polyfills';
import ready from '@/mastodon/ready';
@@ -33,7 +31,14 @@ function loaded() {
if (!report) {
throw new Error('Initial state report not found');
}
- store.dispatch(importFetchedAccounts(initialState.accounts));
+
+ // Set up store
+ store.dispatch(
+ hydrateStore({
+ meta: { locale: document.documentElement.lang },
+ accounts: initialState.accounts,
+ }),
+ );
store.dispatch(importFetchedStatuses(initialState.statuses));
store.dispatch(setReport(report));
@@ -43,7 +48,7 @@ function loaded() {
-
+
,
diff --git a/app/javascript/images/archetypes/space_elements.png b/app/javascript/images/archetypes/space_elements.png
new file mode 100644
index 0000000000..8b83506b8e
Binary files /dev/null and b/app/javascript/images/archetypes/space_elements.png differ
diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js
index e8fec13453..7a68679d44 100644
--- a/app/javascript/mastodon/actions/store.js
+++ b/app/javascript/mastodon/actions/store.js
@@ -22,6 +22,8 @@ export function hydrateStore(rawState) {
dispatch(hydrateCompose());
dispatch(hydrateSearch());
- dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
+ if (rawState.accounts) {
+ dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
+ }
};
}
diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx
index 15f0b9da30..892270b394 100644
--- a/app/javascript/mastodon/components/status.jsx
+++ b/app/javascript/mastodon/components/status.jsx
@@ -117,6 +117,7 @@ class Status extends ImmutablePureComponent {
hidden: PropTypes.bool,
unread: PropTypes.bool,
showThread: PropTypes.bool,
+ showActions: PropTypes.bool,
isQuotedPost: PropTypes.bool,
shouldHighlightOnMount: PropTypes.bool,
getScrollPosition: PropTypes.func,
@@ -381,7 +382,7 @@ class Status extends ImmutablePureComponent {
};
render () {
- const { intl, hidden, featured, unfocusable, unread, showThread, isQuotedPost = false, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props;
+ const { intl, hidden, featured, unfocusable, unread, showThread, showActions = true, isQuotedPost = false, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46, children } = this.props;
let { status, account, ...other } = this.props;
@@ -618,7 +619,7 @@ class Status extends ImmutablePureComponent {
>
)}
- {!isQuotedPost &&
+ {(showActions && !isQuotedPost) &&
}
diff --git a/app/javascript/mastodon/features/annual_report/announcement/index.tsx b/app/javascript/mastodon/features/annual_report/announcement/index.tsx
index 7cdb36e35f..67e1d7b3e5 100644
--- a/app/javascript/mastodon/features/annual_report/announcement/index.tsx
+++ b/app/javascript/mastodon/features/annual_report/announcement/index.tsx
@@ -1,5 +1,7 @@
import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+
import { Button } from '@/mastodon/components/button';
import styles from './styles.module.scss';
@@ -12,7 +14,7 @@ export const AnnualReportAnnouncement: React.FC<{
onOpen: () => void;
}> = ({ year, hasData, isLoading, onRequestBuild, onOpen }) => {
return (
-
+
({
booster: {
- id: 'annual_report.summary.archetype.booster',
- defaultMessage: 'The cool-hunter',
+ id: 'annual_report.summary.archetype.booster.name',
+ defaultMessage: 'The Archer',
},
replier: {
- id: 'annual_report.summary.archetype.replier',
- defaultMessage: 'The social butterfly',
+ id: 'annual_report.summary.archetype.replier.name',
+ defaultMessage: 'The Butterfly',
},
pollster: {
- id: 'annual_report.summary.archetype.pollster',
- defaultMessage: 'The pollster',
+ id: 'annual_report.summary.archetype.pollster.name',
+ defaultMessage: 'The Wonderer',
},
lurker: {
- id: 'annual_report.summary.archetype.lurker',
- defaultMessage: 'The lurker',
+ id: 'annual_report.summary.archetype.lurker.name',
+ defaultMessage: 'The Stoic',
},
oracle: {
- id: 'annual_report.summary.archetype.oracle',
- defaultMessage: 'The oracle',
+ id: 'annual_report.summary.archetype.oracle.name',
+ defaultMessage: 'The Oracle',
},
});
-export const Archetype: React.FC<{
- data: ArchetypeData;
-}> = ({ data }) => {
- const intl = useIntl();
- let illustration;
+export const archetypeSelfDescriptions = defineMessages({
+ booster: {
+ id: 'annual_report.summary.archetype.booster.desc_self',
+ defaultMessage:
+ 'You stayed on the hunt for posts to boost, amplifying other creators with perfect aim.',
+ },
+ replier: {
+ id: 'annual_report.summary.archetype.replier.desc_self',
+ defaultMessage:
+ 'You frequently replied to other people’s posts, pollinating Mastodon with new discussions.',
+ },
+ pollster: {
+ id: 'annual_report.summary.archetype.pollster.desc_self',
+ defaultMessage:
+ 'You created more polls than other post types, cultivating curiosity on Mastodon.',
+ },
+ lurker: {
+ id: 'annual_report.summary.archetype.lurker.desc_self',
+ defaultMessage:
+ 'We know you were out there, somewhere, enjoying Mastodon in your own quiet way.',
+ },
+ oracle: {
+ id: 'annual_report.summary.archetype.oracle.desc_self',
+ defaultMessage:
+ 'You created new posts more than replies, keeping Mastodon fresh and future-facing.',
+ },
+});
- switch (data) {
- case 'booster':
- illustration = booster;
- break;
- case 'replier':
- illustration = replier;
- break;
- case 'pollster':
- illustration = pollster;
- break;
- case 'lurker':
- illustration = lurker;
- break;
- case 'oracle':
- illustration = oracle;
- break;
- }
+export const archetypePublicDescriptions = defineMessages({
+ booster: {
+ id: 'annual_report.summary.archetype.booster.desc_public',
+ defaultMessage:
+ '{name} stayed on the hunt for posts to boost, amplifying other creators with perfect aim.',
+ },
+ replier: {
+ id: 'annual_report.summary.archetype.replier.desc_public',
+ defaultMessage:
+ '{name} frequently replied to other people’s posts, pollinating Mastodon with new discussions.',
+ },
+ pollster: {
+ id: 'annual_report.summary.archetype.pollster.desc_public',
+ defaultMessage:
+ '{name} created more polls than other post types, cultivating curiosity on Mastodon.',
+ },
+ lurker: {
+ id: 'annual_report.summary.archetype.lurker.desc_public',
+ defaultMessage:
+ 'We know {name} was out there, somewhere, enjoying Mastodon in their own quiet way.',
+ },
+ oracle: {
+ id: 'annual_report.summary.archetype.oracle.desc_public',
+ defaultMessage:
+ '{name} created new posts more than replies, keeping Mastodon fresh and future-facing.',
+ },
+});
+
+const illustrations = {
+ booster,
+ replier,
+ pollster,
+ lurker,
+ oracle,
+} as const;
+
+export const Archetype: React.FC<{
+ report: AnnualReport;
+ account?: Account;
+ canShare: boolean;
+}> = ({ report, account, canShare }) => {
+ const intl = useIntl();
+ const wrapperRef = useRef(null);
+ const isSelfView = account?.id === me;
+
+ const [isRevealed, setIsRevealed] = useState(!isSelfView);
+ const reveal = useCallback(() => {
+ setIsRevealed(true);
+ wrapperRef.current?.focus();
+ }, []);
+
+ const archetype = report.data.archetype;
+ const descriptions = isSelfView
+ ? archetypeSelfDescriptions
+ : archetypePublicDescriptions;
+
+ const name = account?.display_name;
return (
-
-
- {intl.formatMessage(archetypeNames[data])}
+
+
+ {account && (
+
+ )}
+
+

+
+
-

+
+
+ {isSelfView ? (
+
+ ) : (
+
+ )}
+
+
+ {isRevealed ? (
+ intl.formatMessage(archetypeNames[archetype])
+ ) : (
+
+ )}
+
+
+ {isRevealed ? (
+ intl.formatMessage(descriptions[archetype], {
+ name,
+ })
+ ) : (
+
+ )}
+
+
+ {!isRevealed && (
+
+ )}
+ {isRevealed && canShare &&
}
);
};
diff --git a/app/javascript/mastodon/features/annual_report/followers.tsx b/app/javascript/mastodon/features/annual_report/followers.tsx
index 196013ae9d..b0f2216bc5 100644
--- a/app/javascript/mastodon/features/annual_report/followers.tsx
+++ b/app/javascript/mastodon/features/annual_report/followers.tsx
@@ -1,68 +1,24 @@
import { FormattedMessage, FormattedNumber } from 'react-intl';
-import { Sparklines, SparklinesCurve } from 'react-sparklines';
+import classNames from 'classnames';
-import { ShortNumber } from 'mastodon/components/short_number';
-import type { TimeSeriesMonth } from 'mastodon/models/annual_report';
+import styles from './index.module.scss';
export const Followers: React.FC<{
- data: TimeSeriesMonth[];
- total?: number;
-}> = ({ data, total }) => {
- const change = data.reduce((sum, item) => sum + item.followers, 0);
-
- const cumulativeGraph = data.reduce(
- (newData, item) => [
- ...newData,
- item.followers + (newData[newData.length - 1] ?? 0),
- ],
- [0],
- );
-
+ count: number;
+}> = ({ count }) => {
return (
-
-
-
+
+
+
+
-
-
-
-
-
- {change > -1 ? '+' : '-'}
-
-
-
-
+
+
);
diff --git a/app/javascript/mastodon/features/annual_report/highlighted_post.tsx b/app/javascript/mastodon/features/annual_report/highlighted_post.tsx
index 7edbb2e614..5ce4947609 100644
--- a/app/javascript/mastodon/features/annual_report/highlighted_post.tsx
+++ b/app/javascript/mastodon/features/annual_report/highlighted_post.tsx
@@ -1,102 +1,77 @@
/* eslint-disable @typescript-eslint/no-unsafe-return,
@typescript-eslint/no-explicit-any,
- @typescript-eslint/no-unsafe-assignment */
-
-import { useCallback } from 'react';
+ @typescript-eslint/no-unsafe-assignment,
+ @typescript-eslint/no-unsafe-member-access,
+ @typescript-eslint/no-unsafe-call */
import { FormattedMessage } from 'react-intl';
-import { DisplayName } from '@/mastodon/components/display_name';
-import { toggleStatusSpoilers } from 'mastodon/actions/statuses';
-import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
-import { me } from 'mastodon/initial_state';
+import classNames from 'classnames';
+
+import { StatusQuoteManager } from 'mastodon/components/status_quoted';
import type { TopStatuses } from 'mastodon/models/annual_report';
-import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
-import { useAppSelector, useAppDispatch } from 'mastodon/store';
+import { makeGetStatus } from 'mastodon/selectors';
+import { useAppSelector } from 'mastodon/store';
+
+import styles from './index.module.scss';
const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
-const getPictureInPicture = makeGetPictureInPicture() as unknown as (
- arg0: any,
- arg1: any,
-) => any;
export const HighlightedPost: React.FC<{
data: TopStatuses;
}> = ({ data }) => {
- let statusId, label;
+ const { by_reblogs, by_favourites, by_replies } = data;
- if (data.by_reblogs) {
- statusId = data.by_reblogs;
- label = (
-
- );
- } else if (data.by_favourites) {
- statusId = data.by_favourites;
- label = (
-
- );
- } else {
- statusId = data.by_replies;
- label = (
-
- );
- }
+ const statusId = by_reblogs || by_favourites || by_replies;
- const dispatch = useAppDispatch();
- const domain = useAppSelector((state) => state.meta.get('domain'));
const status = useAppSelector((state) =>
statusId ? getStatus(state, { id: statusId }) : undefined,
);
- const pictureInPicture = useAppSelector((state) =>
- statusId ? getPictureInPicture(state, { id: statusId }) : undefined,
- );
- const account = useAppSelector((state) =>
- me ? state.accounts.get(me) : undefined,
- );
-
- const handleToggleHidden = useCallback(() => {
- dispatch(toggleStatusSpoilers(statusId));
- }, [dispatch, statusId]);
if (!status) {
- return (
-
+ return
;
+ }
+
+ let label;
+ if (by_reblogs) {
+ label = (
+
+ );
+ } else if (by_favourites) {
+ label = (
+
+ );
+ } else {
+ label = (
+
);
}
- const displayName = (
-
-
- ,
- }}
- />
-
- {label}
-
- );
-
return (
-
-
+
);
};
diff --git a/app/javascript/mastodon/features/annual_report/index.module.scss b/app/javascript/mastodon/features/annual_report/index.module.scss
new file mode 100644
index 0000000000..0258f9c798
--- /dev/null
+++ b/app/javascript/mastodon/features/annual_report/index.module.scss
@@ -0,0 +1,283 @@
+.modalWrapper {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 40px;
+ overflow-y: auto;
+ pointer-events: none;
+ scrollbar-color: var(--color-text-secondary) var(--color-bg-secondary);
+
+ .loading-indicator .circular-progress {
+ color: var(--lime);
+ }
+}
+
+.closeButton {
+ position: absolute;
+ top: 24px;
+ right: 24px;
+ padding: 8px;
+ border-radius: 100%;
+
+ --default-icon-color: var(--color-bg-primary);
+ --default-bg-color: var(--color-text-primary);
+ --hover-icon-color: var(--color-bg-primary);
+ --hover-bg-color: var(--color-text-primary);
+}
+
+.wrapper {
+ position: relative;
+ max-width: 600px;
+ padding: 24px;
+ contain: layout;
+ flex: 0 0 auto;
+ pointer-events: auto;
+ color: var(--color-text-primary);
+ background: var(--color-bg-primary);
+ background:
+ radial-gradient(at 40% 87%, #240c9a99 0, transparent 50%),
+ radial-gradient(at 19% 10%, #6b0c9a99 0, transparent 50%),
+ radial-gradient(at 90% 27%, #9a0c8299 0, transparent 50%),
+ radial-gradient(at 16% 95%, #1e948299 0, transparent 50%),
+ radial-gradient(at 80% 91%, #16dae499 0, transparent 50%)
+ var(--color-bg-primary);
+ border-radius: 40px;
+
+ @media (width < 600px) {
+ padding: 12px;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ background: inherit;
+ border-radius: inherit;
+ filter: blur(20px);
+ }
+}
+
+.header {
+ margin-bottom: 18px;
+ text-align: center;
+
+ h1 {
+ font-family: monospace;
+ text-transform: uppercase;
+ letter-spacing: 0.15em;
+ font-size: 30px;
+ font-weight: 600;
+ line-height: 1.5;
+ margin-bottom: 8px;
+ }
+
+ p {
+ font-size: 14px;
+ line-height: 1.5;
+ }
+}
+
+.stack {
+ --grid-spacing: 12px;
+
+ display: grid;
+ gap: var(--grid-spacing);
+}
+
+.box {
+ position: relative;
+ padding: 16px;
+ border-radius: 16px;
+ background: rgb(from var(--color-bg-primary) r g b / 60%);
+ box-shadow: inset 0 0 0 1px rgb(from var(--color-text-primary) r g b / 40%);
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ inset-inline: 0;
+ display: block;
+ height: 1px;
+ background-image: linear-gradient(
+ to right,
+ transparent,
+ white,
+ transparent
+ );
+ }
+
+ &::before {
+ top: 0;
+ }
+
+ &::after {
+ bottom: 0;
+ }
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 8px;
+ padding: 16px;
+ font-size: 14px;
+ text-align: center;
+ text-wrap: balance;
+
+ &.comfortable {
+ gap: 12px;
+ }
+}
+
+.title {
+ text-transform: uppercase;
+ color: #c2c8ff;
+ font-weight: 500;
+}
+
+.statLarge {
+ font-size: 24px;
+ font-weight: 500;
+ overflow-wrap: break-word;
+}
+
+.statExtraLarge {
+ font-size: 32px;
+ font-weight: 500;
+ line-height: 1;
+ overflow-wrap: break-word;
+}
+
+.mostBoostedPost {
+ padding: 0;
+ padding-top: 8px;
+ overflow: hidden;
+}
+
+.statsGrid {
+ display: grid;
+ gap: var(--grid-spacing);
+ grid-template-columns: 1fr 2fr;
+ grid-template-areas:
+ 'followers hashtag'
+ 'new-posts hashtag';
+
+ @media (width < 680px) {
+ grid-template-columns: 1fr 1fr;
+ grid-template-areas:
+ 'followers new-posts'
+ 'hashtag hashtag';
+ }
+
+ &:empty {
+ display: none;
+ }
+
+ &.onlyHashtag {
+ grid-template-columns: 1fr;
+ grid-template-areas: 'hashtag';
+ }
+
+ &.noHashtag {
+ grid-template-columns: 1fr 1fr;
+ grid-template-areas: 'followers new-posts';
+ }
+
+ &.singleNumber {
+ grid-template-columns: 1fr 2fr;
+ grid-template-areas: 'number hashtag';
+
+ @media (width < 680px) {
+ grid-template-areas:
+ 'number number'
+ 'hashtag hashtag';
+ }
+ }
+
+ &.singleNumber.noHashtag {
+ grid-template-columns: 1fr;
+ grid-template-areas: 'number';
+ }
+}
+
+.followers {
+ grid-area: followers;
+
+ .singleNumber & {
+ grid-area: number;
+ }
+}
+
+.newPosts {
+ grid-area: new-posts;
+
+ .singleNumber & {
+ grid-area: number;
+ }
+}
+
+.mostUsedHashtag {
+ grid-area: hashtag;
+ padding-block: 24px;
+}
+
+.archetype {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+}
+
+.archetypeArtboard {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ align-self: center;
+ width: 180px;
+ padding-top: 40px;
+}
+
+.archetypeAvatar {
+ position: absolute;
+ top: 7px;
+ left: 4px;
+ border-radius: 100%;
+ overflow: hidden;
+}
+
+.archetypeIllustrationWrapper {
+ position: relative;
+ width: 92px;
+ aspect-ratio: 1;
+ overflow: hidden;
+ border-radius: 100%;
+
+ &::after {
+ content: '';
+ display: block;
+ position: absolute;
+ inset: 0;
+ border-radius: inherit;
+ box-shadow: inset -10px -4px 15px #00000080;
+ }
+}
+
+.archetypeIllustration {
+ width: 100%;
+}
+
+.blurredImage {
+ filter: blur(10px);
+}
+
+.archetypePlanetRing {
+ position: absolute;
+ top: 0;
+ left: 0;
+ mix-blend-mode: screen;
+}
diff --git a/app/javascript/mastodon/features/annual_report/index.tsx b/app/javascript/mastodon/features/annual_report/index.tsx
index e9f0b5f2d7..b02e8fb898 100644
--- a/app/javascript/mastodon/features/annual_report/index.tsx
+++ b/app/javascript/mastodon/features/annual_report/index.tsx
@@ -1,95 +1,123 @@
-import { useCallback } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import type { FC } from 'react';
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
-import { focusCompose, resetCompose } from '@/mastodon/actions/compose';
+import { useLocation } from 'react-router';
+
+import classNames from 'classnames/bind';
+
import { closeModal } from '@/mastodon/actions/modal';
-import { Button } from '@/mastodon/components/button';
+import { IconButton } from '@/mastodon/components/icon_button';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import { me } from '@/mastodon/initial_state';
-import type { AnnualReport as AnnualReportData } from '@/mastodon/models/annual_report';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
+import CloseIcon from '@/material-icons/400-24px/close.svg?react';
-import { Archetype, archetypeNames } from './archetype';
+import { Archetype } from './archetype';
import { Followers } from './followers';
import { HighlightedPost } from './highlighted_post';
+import styles from './index.module.scss';
import { MostUsedHashtag } from './most_used_hashtag';
import { NewPosts } from './new_posts';
-const shareMessage = defineMessage({
+const moduleClassNames = classNames.bind(styles);
+
+export const shareMessage = defineMessage({
id: 'annual_report.summary.share_message',
defaultMessage: 'I got the {archetype} archetype!',
});
// Share = false when using the embedded version of the report.
-export const AnnualReport: FC<{ share?: boolean }> = ({ share = true }) => {
- const currentAccount = useAppSelector((state) =>
- me ? state.accounts.get(me) : 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 close = useCallback(() => {
+ dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
+ }, [dispatch]);
+
+ // Close modal when navigating away from within
+ const { pathname } = useLocation();
+ const [initialPathname] = useState(pathname);
+ useEffect(() => {
+ if (pathname !== initialPathname) {
+ close();
+ }
+ }, [pathname, initialPathname, close]);
if (!report) {
return
;
}
+ const newPostCount = report.data.time_series.reduce(
+ (sum, item) => sum + item.statuses,
+ 0,
+ );
+
+ const newFollowerCount = report.data.time_series.reduce(
+ (sum, item) => sum + item.followers,
+ 0,
+ );
+
+ const topHashtag = report.data.top_hashtags[0];
+
return (
-
-
+
+
-
-
-
-
+
+ {account &&
@{account.acct}
}
+ {context === 'modal' && (
+
+ )}
-
-
+
-
+ {!!newFollowerCount && }
+ {!!newPostCount && }
+ {topHashtag && }
+
+
-
-
- {share &&
}
);
};
-
-const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => {
- const intl = useIntl();
- const dispatch = useAppDispatch();
- const handleShareClick = useCallback(() => {
- // Generate the share message.
- const archetypeName = intl.formatMessage(
- archetypeNames[report.data.archetype],
- );
- const shareLines = [
- intl.formatMessage(shareMessage, {
- archetype: archetypeName,
- }),
- ];
- // Share URL is only available for schema version 2.
- if (report.schema_version === 2 && report.share_url) {
- shareLines.push(report.share_url);
- }
- shareLines.push(`#Wrapstodon${report.year}`);
-
- // Reset the composer and focus it with the share message, then close the modal.
- dispatch(resetCompose());
- dispatch(focusCompose(shareLines.join('\n\n')));
- dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
- }, [report, intl, dispatch]);
-
- return
;
-};
diff --git a/app/javascript/mastodon/features/annual_report/modal.tsx b/app/javascript/mastodon/features/annual_report/modal.tsx
new file mode 100644
index 0000000000..262584b7b9
--- /dev/null
+++ b/app/javascript/mastodon/features/annual_report/modal.tsx
@@ -0,0 +1,29 @@
+import { useEffect } from 'react';
+
+import classNames from 'classnames';
+
+import { AnnualReport } from '.';
+import styles from './index.module.scss';
+
+const AnnualReportModal: React.FC<{
+ onChangeBackgroundColor: (color: string) => void;
+}> = ({ onChangeBackgroundColor }) => {
+ useEffect(() => {
+ onChangeBackgroundColor('var(--color-bg-media-base)');
+ }, [onChangeBackgroundColor]);
+
+ return (
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default AnnualReportModal;
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 4b59b89737..9a0720d8ab 100644
--- a/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx
+++ b/app/javascript/mastodon/features/annual_report/most_used_hashtag.tsx
@@ -1,30 +1,34 @@
import { FormattedMessage } from 'react-intl';
+import classNames from 'classnames';
+
import type { NameAndCount } from 'mastodon/models/annual_report';
-export const MostUsedHashtag: React.FC<{
- data: NameAndCount[];
-}> = ({ data }) => {
- const hashtag = data[0];
+import styles from './index.module.scss';
+export const MostUsedHashtag: React.FC<{
+ hashtag: NameAndCount;
+}> = ({ hashtag }) => {
return (
-
-
- {hashtag ? (
- <>#{hashtag.name}>
- ) : (
-
- )}
-
-
+
+
+
+
#{hashtag.name}
+
+
+
+
);
};
diff --git a/app/javascript/mastodon/features/annual_report/new_posts.tsx b/app/javascript/mastodon/features/annual_report/new_posts.tsx
index 9ead0176b2..9a265f0b9d 100644
--- a/app/javascript/mastodon/features/annual_report/new_posts.tsx
+++ b/app/javascript/mastodon/features/annual_report/new_posts.tsx
@@ -1,51 +1,23 @@
import { FormattedNumber, FormattedMessage } from 'react-intl';
-import ChatBubbleIcon from '@/material-icons/400-24px/chat_bubble.svg?react';
-import type { TimeSeriesMonth } from 'mastodon/models/annual_report';
+import classNames from 'classnames';
+
+import styles from './index.module.scss';
export const NewPosts: React.FC<{
- data: TimeSeriesMonth[];
-}> = ({ data }) => {
- const posts = data.reduce((sum, item) => sum + item.statuses, 0);
-
+ count: number;
+}> = ({ count }) => {
return (
-
-
-
-
-
+
+
+
-
diff --git a/app/javascript/mastodon/features/annual_report/share_button.tsx b/app/javascript/mastodon/features/annual_report/share_button.tsx
new file mode 100644
index 0000000000..f9ad1d4e9c
--- /dev/null
+++ b/app/javascript/mastodon/features/annual_report/share_button.tsx
@@ -0,0 +1,41 @@
+import { useCallback } from 'react';
+import type { FC } from 'react';
+
+import { useIntl } from 'react-intl';
+
+import { resetCompose, focusCompose } from '@/mastodon/actions/compose';
+import { closeModal } from '@/mastodon/actions/modal';
+import { Button } from '@/mastodon/components/button';
+import type { AnnualReport as AnnualReportData } from '@/mastodon/models/annual_report';
+import { useAppDispatch } from '@/mastodon/store';
+
+import { shareMessage } from '.';
+import { archetypeNames } from './archetype';
+
+export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => {
+ const intl = useIntl();
+ const dispatch = useAppDispatch();
+ const handleShareClick = useCallback(() => {
+ // Generate the share message.
+ const archetypeName = intl.formatMessage(
+ archetypeNames[report.data.archetype],
+ );
+ const shareLines = [
+ intl.formatMessage(shareMessage, {
+ archetype: archetypeName,
+ }),
+ ];
+ // Share URL is only available for schema version 2.
+ if (report.schema_version === 2 && report.share_url) {
+ shareLines.push(report.share_url);
+ }
+ shareLines.push(`#Wrapstodon${report.year}`);
+
+ // Reset the composer and focus it with the share message, then close the modal.
+ dispatch(resetCompose());
+ dispatch(focusCompose(shareLines.join('\n\n')));
+ dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
+ }, [report, intl, dispatch]);
+
+ return
;
+};
diff --git a/app/javascript/mastodon/features/annual_report/share.module.css b/app/javascript/mastodon/features/annual_report/shared_page.module.css
similarity index 83%
rename from app/javascript/mastodon/features/annual_report/share.module.css
rename to app/javascript/mastodon/features/annual_report/shared_page.module.css
index e4d01ff3e3..99b713c05d 100644
--- a/app/javascript/mastodon/features/annual_report/share.module.css
+++ b/app/javascript/mastodon/features/annual_report/shared_page.module.css
@@ -1,6 +1,6 @@
.wrapper {
- max-width: 40rem;
- margin: 0 auto;
+ max-width: max-content;
+ margin: 40px auto;
}
.footer {
diff --git a/app/javascript/mastodon/features/annual_report/share.tsx b/app/javascript/mastodon/features/annual_report/shared_page.tsx
similarity index 74%
rename from app/javascript/mastodon/features/annual_report/share.tsx
rename to app/javascript/mastodon/features/annual_report/shared_page.tsx
index 25b3ee9885..b2e29d0dc7 100644
--- a/app/javascript/mastodon/features/annual_report/share.tsx
+++ b/app/javascript/mastodon/features/annual_report/shared_page.tsx
@@ -3,12 +3,12 @@ import type { FC } from 'react';
import { IconLogo } from '@/mastodon/components/logo';
import { AnnualReport } from './index';
-import classes from './share.module.css';
+import classes from './shared_page.module.css';
-export const WrapstodonShare: FC = () => {
+export const WrapstodonSharedPage: FC = () => {
return (
-
+