0
0
Fork 0

Add year in review feature to web UI (#32709)

This commit is contained in:
Eugen Rochko 2024-11-05 15:40:07 +01:00 committed by GitHub
parent 20a147170e
commit d6349c0e9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1103 additions and 21 deletions

View file

@ -20,6 +20,7 @@ export const allNotificationTypes = [
'admin.report',
'moderation_warning',
'severed_relationships',
'annual_report',
];
export type NotificationWithStatusType =
@ -37,7 +38,8 @@ export type NotificationType =
| 'moderation_warning'
| 'severed_relationships'
| 'admin.sign_up'
| 'admin.report';
| 'admin.report'
| 'annual_report';
export interface BaseNotificationJSON {
id: string;
@ -130,6 +132,15 @@ interface AccountRelationshipSeveranceNotificationJSON
event: ApiAccountRelationshipSeveranceEventJSON;
}
export interface ApiAnnualReportEventJSON {
year: string;
}
interface AnnualReportNotificationGroupJSON extends BaseNotificationGroupJSON {
type: 'annual_report';
annual_report: ApiAnnualReportEventJSON;
}
export type ApiNotificationJSON =
| SimpleNotificationJSON
| ReportNotificationJSON
@ -142,7 +153,8 @@ export type ApiNotificationGroupJSON =
| ReportNotificationGroupJSON
| AccountRelationshipSeveranceNotificationGroupJSON
| NotificationGroupWithStatusJSON
| ModerationWarningNotificationGroupJSON;
| ModerationWarningNotificationGroupJSON
| AnnualReportNotificationGroupJSON;
export interface ApiNotificationGroupsResultJSON {
accounts: ApiAccountJSON[];

View file

@ -13,11 +13,14 @@ class ModalRoot extends PureComponent {
static propTypes = {
children: PropTypes.node,
onClose: PropTypes.func.isRequired,
backgroundColor: PropTypes.shape({
r: PropTypes.number,
g: PropTypes.number,
b: PropTypes.number,
}),
backgroundColor: PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
r: PropTypes.number,
g: PropTypes.number,
b: PropTypes.number,
}),
]),
ignoreFocus: PropTypes.bool,
...WithOptionalRouterPropTypes,
};
@ -141,14 +144,17 @@ class ModalRoot extends PureComponent {
let backgroundColor = null;
if (this.props.backgroundColor) {
backgroundColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 });
if (this.props.backgroundColor && typeof this.props.backgroundColor === 'string') {
backgroundColor = this.props.backgroundColor;
} else if (this.props.backgroundColor) {
const darkenedColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 });
backgroundColor = `rgb(${darkenedColor.r}, ${darkenedColor.g}, ${darkenedColor.b})`;
}
return (
<div className='modal-root' ref={this.setRef}>
<div style={{ pointerEvents: visible ? 'auto' : 'none' }}>
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor: backgroundColor ? `rgba(${backgroundColor.r}, ${backgroundColor.g}, ${backgroundColor.b}, 0.9)` : null }} />
<div role='presentation' className='modal-root__overlay' onClick={onClose} style={{ backgroundColor }} />
<div role='dialog' className='modal-root__container'>{children}</div>
</div>
</div>

View file

@ -0,0 +1,69 @@
import { FormattedMessage } from 'react-intl';
import booster from '@/images/archetypes/booster.png';
import lurker from '@/images/archetypes/lurker.png';
import oracle from '@/images/archetypes/oracle.png';
import pollster from '@/images/archetypes/pollster.png';
import replier from '@/images/archetypes/replier.png';
import type { Archetype as ArchetypeData } from 'mastodon/models/annual_report';
export const Archetype: React.FC<{
data: ArchetypeData;
}> = ({ data }) => {
let illustration, label;
switch (data) {
case 'booster':
illustration = booster;
label = (
<FormattedMessage
id='annual_report.summary.archetype.booster'
defaultMessage='The cool-hunter'
/>
);
break;
case 'replier':
illustration = replier;
label = (
<FormattedMessage
id='annual_report.summary.archetype.replier'
defaultMessage='The social butterfly'
/>
);
break;
case 'pollster':
illustration = pollster;
label = (
<FormattedMessage
id='annual_report.summary.archetype.pollster'
defaultMessage='The pollster'
/>
);
break;
case 'lurker':
illustration = lurker;
label = (
<FormattedMessage
id='annual_report.summary.archetype.lurker'
defaultMessage='The lurker'
/>
);
break;
case 'oracle':
illustration = oracle;
label = (
<FormattedMessage
id='annual_report.summary.archetype.oracle'
defaultMessage='The oracle'
/>
);
break;
}
return (
<div className='annual-report__bento__box annual-report__summary__archetype'>
<div className='annual-report__summary__archetype__label'>{label}</div>
<img src={illustration} alt='' />
</div>
);
};

View file

@ -0,0 +1,69 @@
import { FormattedMessage, FormattedNumber } from 'react-intl';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { ShortNumber } from 'mastodon/components/short_number';
import type { TimeSeriesMonth } from 'mastodon/models/annual_report';
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],
);
return (
<div className='annual-report__bento__box annual-report__summary__followers'>
<Sparklines data={cumulativeGraph} margin={0}>
<svg>
<defs>
<linearGradient id='gradient' x1='0%' y1='0%' x2='0%' y2='100%'>
<stop
offset='0%'
stopColor='var(--sparkline-gradient-top)'
stopOpacity='1'
/>
<stop
offset='100%'
stopColor='var(--sparkline-gradient-bottom)'
stopOpacity='0'
/>
</linearGradient>
</defs>
</svg>
<SparklinesCurve style={{ fill: 'none' }} />
</Sparklines>
<div className='annual-report__summary__followers__foreground'>
<div className='annual-report__summary__followers__number'>
{change > -1 ? '+' : '-'}
<FormattedNumber value={change} />
</div>
<div className='annual-report__summary__followers__label'>
<span>
<FormattedMessage
id='annual_report.summary.followers.followers'
defaultMessage='followers'
/>
</span>
<div className='annual-report__summary__followers__footnote'>
<FormattedMessage
id='annual_report.summary.followers.total'
defaultMessage='{count} total'
values={{ count: <ShortNumber value={total ?? 0} /> }}
/>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,105 @@
/* eslint-disable @typescript-eslint/no-unsafe-return,
@typescript-eslint/no-explicit-any,
@typescript-eslint/no-unsafe-assignment */
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { toggleStatusSpoilers } from 'mastodon/actions/statuses';
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
import { me } from 'mastodon/initial_state';
import type { TopStatuses } from 'mastodon/models/annual_report';
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
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;
if (data.by_reblogs) {
statusId = data.by_reblogs;
label = (
<FormattedMessage
id='annual_report.summary.highlighted_post.by_reblogs'
defaultMessage='most boosted post'
/>
);
} else if (data.by_favourites) {
statusId = data.by_favourites;
label = (
<FormattedMessage
id='annual_report.summary.highlighted_post.by_favourites'
defaultMessage='most favourited post'
/>
);
} else {
statusId = data.by_replies;
label = (
<FormattedMessage
id='annual_report.summary.highlighted_post.by_replies'
defaultMessage='post with the most 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 (
<div className='annual-report__bento__box annual-report__summary__most-boosted-post' />
);
}
const displayName = (
<span className='display-name'>
<strong className='display-name__html'>
<FormattedMessage
id='annual_report.summary.highlighted_post.possessive'
defaultMessage="{name}'s"
values={{
name: account && (
<bdi
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
/>
),
}}
/>
</strong>
<span className='display-name__account'>{label}</span>
</span>
);
return (
<div className='annual-report__bento__box annual-report__summary__most-boosted-post'>
<DetailedStatus
status={status}
pictureInPicture={pictureInPicture}
domain={domain}
onToggleHidden={handleToggleHidden}
overrideDisplayName={displayName}
/>
</div>
);
};

View file

@ -0,0 +1,99 @@
import { useState, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import {
importFetchedStatuses,
importFetchedAccounts,
} from 'mastodon/actions/importer';
import { apiRequestGet, apiRequestPost } from 'mastodon/api';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { me } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
import type { AnnualReport as AnnualReportData } from 'mastodon/models/annual_report';
import type { Status } from 'mastodon/models/status';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { Archetype } from './archetype';
import { Followers } from './followers';
import { HighlightedPost } from './highlighted_post';
import { MostUsedHashtag } from './most_used_hashtag';
import { NewPosts } from './new_posts';
import { Percentile } from './percentile';
interface AnnualReportResponse {
annual_reports: AnnualReportData[];
accounts: Account[];
statuses: Status[];
}
export const AnnualReport: React.FC<{
year: string;
}> = ({ year }) => {
const [response, setResponse] = useState<AnnualReportResponse | null>(null);
const [loading, setLoading] = useState(false);
const currentAccount = useAppSelector((state) =>
me ? state.accounts.get(me) : undefined,
);
const dispatch = useAppDispatch();
useEffect(() => {
setLoading(true);
apiRequestGet<AnnualReportResponse>(`v1/annual_reports/${year}`)
.then((data) => {
dispatch(importFetchedStatuses(data.statuses));
dispatch(importFetchedAccounts(data.accounts));
setResponse(data);
setLoading(false);
return apiRequestPost(`v1/annual_reports/${year}/read`);
})
.catch(() => {
setLoading(false);
});
}, [dispatch, year, setResponse, setLoading]);
if (loading) {
return <LoadingIndicator />;
}
const report = response?.annual_reports[0];
if (!report) {
return null;
}
return (
<div className='annual-report'>
<div className='annual-report__header'>
<h1>
<FormattedMessage
id='annual_report.summary.thanks'
defaultMessage='Thanks for being part of Mastodon!'
/>
</h1>
<p>
<FormattedMessage
id='annual_report.summary.here_it_is'
defaultMessage='Here is your {year} in review:'
values={{ year: report.year }}
/>
</p>
</div>
<div className='annual-report__bento annual-report__summary'>
<Archetype data={report.data.archetype} />
<HighlightedPost data={report.data.top_statuses} />
<Followers
data={report.data.time_series}
total={currentAccount?.followers_count}
/>
<MostUsedHashtag data={report.data.top_hashtags} />
<Percentile data={report.data.percentiles} />
<NewPosts data={report.data.time_series} />
</div>
</div>
);
};

View file

@ -0,0 +1,29 @@
import { FormattedMessage } from 'react-intl';
import type { NameAndCount } from 'mastodon/models/annual_report';
export const MostUsedApp: React.FC<{
data: NameAndCount[];
}> = ({ data }) => {
const app = data[0];
if (!app) {
return (
<div className='annual-report__bento__box annual-report__summary__most-used-app' />
);
}
return (
<div className='annual-report__bento__box annual-report__summary__most-used-app'>
<div className='annual-report__summary__most-used-app__icon'>
{app.name}
</div>
<div className='annual-report__summary__most-used-app__label'>
<FormattedMessage
id='annual_report.summary.most_used_app.most_used_app'
defaultMessage='most used app'
/>
</div>
</div>
);
};

View file

@ -0,0 +1,29 @@
import { FormattedMessage } from 'react-intl';
import type { NameAndCount } from 'mastodon/models/annual_report';
export const MostUsedHashtag: React.FC<{
data: NameAndCount[];
}> = ({ data }) => {
const hashtag = data[0];
if (!hashtag) {
return (
<div className='annual-report__bento__box annual-report__summary__most-used-hashtag' />
);
}
return (
<div className='annual-report__bento__box annual-report__summary__most-used-hashtag'>
<div className='annual-report__summary__most-used-hashtag__hashtag'>
#{hashtag.name}
</div>
<div className='annual-report__summary__most-used-hashtag__label'>
<FormattedMessage
id='annual_report.summary.most_used_hashtag.most_used_hashtag'
defaultMessage='most used hashtag'
/>
</div>
</div>
);
};

View file

@ -0,0 +1,53 @@
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';
export const NewPosts: React.FC<{
data: TimeSeriesMonth[];
}> = ({ data }) => {
const posts = data.reduce((sum, item) => sum + item.statuses, 0);
return (
<div className='annual-report__bento__box annual-report__summary__new-posts'>
<svg width={500} height={500}>
<defs>
<pattern
id='posts'
x='0'
y='0'
width='32'
height='35'
patternUnits='userSpaceOnUse'
>
<circle cx='12' cy='12' r='12' fill='var(--lime)' />
<ChatBubbleIcon
fill='var(--indigo-1)'
x='4'
y='4'
width='16'
height='16'
/>
</pattern>
</defs>
<rect
width={500}
height={500}
fill='url(#posts)'
style={{ opacity: 0.2 }}
/>
</svg>
<div className='annual-report__summary__new-posts__number'>
<FormattedNumber value={posts} />
</div>
<div className='annual-report__summary__new-posts__label'>
<FormattedMessage
id='annual_report.summary.new_posts.new_posts'
defaultMessage='new posts'
/>
</div>
</div>
);
};

View file

@ -0,0 +1,53 @@
/* eslint-disable react/jsx-no-useless-fragment */
import { FormattedMessage, FormattedNumber } from 'react-intl';
import type { Percentiles } from 'mastodon/models/annual_report';
export const Percentile: React.FC<{
data: Percentiles;
}> = ({ data }) => {
const percentile = data.statuses;
return (
<div className='annual-report__bento__box annual-report__summary__percentile'>
<FormattedMessage
id='annual_report.summary.percentile.text'
defaultMessage='<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of Mastodon users.</bottomLabel>'
values={{
topLabel: (str) => (
<div className='annual-report__summary__percentile__label'>
{str}
</div>
),
percentage: () => (
<div className='annual-report__summary__percentile__number'>
<FormattedNumber
value={percentile / 100}
style='percent'
maximumFractionDigits={1}
/>
</div>
),
bottomLabel: (str) => (
<div>
<div className='annual-report__summary__percentile__label'>
{str}
</div>
{percentile < 6 && (
<div className='annual-report__summary__percentile__footnote'>
<FormattedMessage
id='annual_report.summary.percentile.we_wont_tell_bernie'
defaultMessage="We won't tell Bernie."
/>
</div>
)}
</div>
),
}}
>
{(message) => <>{message}</>}
</FormattedMessage>
</div>
);
};

View file

@ -0,0 +1,59 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import CelebrationIcon from '@/material-icons/400-24px/celebration.svg?react';
import { openModal } from 'mastodon/actions/modal';
import { Icon } from 'mastodon/components/icon';
import type { NotificationGroupAnnualReport } from 'mastodon/models/notification_group';
import { useAppDispatch } from 'mastodon/store';
export const NotificationAnnualReport: React.FC<{
notification: NotificationGroupAnnualReport;
unread: boolean;
}> = ({ notification: { annualReport }, unread }) => {
const dispatch = useAppDispatch();
const year = annualReport.year;
const handleClick = useCallback(() => {
dispatch(
openModal({
modalType: 'ANNUAL_REPORT',
modalProps: { year },
}),
);
}, [dispatch, year]);
return (
<div
role='button'
className={classNames(
'notification-group notification-group--link notification-group--annual-report focusable',
{ 'notification-group--unread': unread },
)}
tabIndex={0}
>
<div className='notification-group__icon'>
<Icon id='celebration' icon={CelebrationIcon} />
</div>
<div className='notification-group__main'>
<p>
<FormattedMessage
id='notification.annual_report.message'
defaultMessage="Your {year} #Wrapstodon awaits! Unveil your year's highlights and memorable moments on Mastodon!"
values={{ year }}
/>
</p>
<button onClick={handleClick} className='link-button'>
<FormattedMessage
id='notification.annual_report.view'
defaultMessage='View #Wrapstodon'
/>
</button>
</div>
</div>
);
};

View file

@ -9,6 +9,7 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { NotificationAdminReport } from './notification_admin_report';
import { NotificationAdminSignUp } from './notification_admin_sign_up';
import { NotificationAnnualReport } from './notification_annual_report';
import { NotificationFavourite } from './notification_favourite';
import { NotificationFollow } from './notification_follow';
import { NotificationFollowRequest } from './notification_follow_request';
@ -143,6 +144,14 @@ export const NotificationGroup: React.FC<{
/>
);
break;
case 'annual_report':
content = (
<NotificationAnnualReport
unread={unread}
notification={notificationGroup}
/>
);
break;
default:
return null;
}

View file

@ -49,6 +49,7 @@ export const DetailedStatus: React.FC<{
domain: string;
showMedia?: boolean;
withLogo?: boolean;
overrideDisplayName?: React.ReactNode;
pictureInPicture: any;
onToggleHidden?: (status: any) => void;
onToggleMediaVisibility?: () => void;
@ -62,6 +63,7 @@ export const DetailedStatus: React.FC<{
domain,
showMedia,
withLogo,
overrideDisplayName,
pictureInPicture,
onToggleMediaVisibility,
onToggleHidden,
@ -319,7 +321,11 @@ export const DetailedStatus: React.FC<{
<div className='detailed-status__display-avatar'>
<Avatar account={status.get('account')} size={46} />
</div>
<DisplayName account={status.get('account')} localDomain={domain} />
{overrideDisplayName ?? (
<DisplayName account={status.get('account')} localDomain={domain} />
)}
{withLogo && (
<>
<div className='spacer' />

View file

@ -0,0 +1,21 @@
import { useEffect } from 'react';
import { AnnualReport } from 'mastodon/features/annual_report';
const AnnualReportModal: React.FC<{
year: string;
onChangeBackgroundColor: (arg0: string) => void;
}> = ({ year, onChangeBackgroundColor }) => {
useEffect(() => {
onChangeBackgroundColor('var(--indigo-1)');
}, [onChangeBackgroundColor]);
return (
<div className='modal-root__modal annual-report-modal'>
<AnnualReport year={year} />
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default AnnualReportModal;

View file

@ -18,6 +18,7 @@ import {
SubscribedLanguagesModal,
ClosedRegistrationsModal,
IgnoreNotificationsModal,
AnnualReportModal,
} from 'mastodon/features/ui/util/async-components';
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
@ -72,6 +73,7 @@ export const MODAL_COMPONENTS = {
'INTERACTION': InteractionModal,
'CLOSED_REGISTRATIONS': ClosedRegistrationsModal,
'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal,
'ANNUAL_REPORT': AnnualReportModal,
};
export default class ModalRoot extends PureComponent {

View file

@ -217,3 +217,7 @@ export function NotificationRequest () {
export function LinkTimeline () {
return import(/*webpackChunkName: "features/link_timeline" */'../../link_timeline');
}
export function AnnualReportModal () {
return import(/*webpackChunkName: "modals/annual_report_modal" */'../components/annual_report_modal');
}

View file

@ -87,6 +87,24 @@
"alert.unexpected.title": "Oops!",
"alt_text_badge.title": "Alt text",
"announcement.announcement": "Announcement",
"annual_report.summary.archetype.booster": "The cool-hunter",
"annual_report.summary.archetype.lurker": "The lurker",
"annual_report.summary.archetype.oracle": "The oracle",
"annual_report.summary.archetype.pollster": "The pollster",
"annual_report.summary.archetype.replier": "The social butterfly",
"annual_report.summary.followers.followers": "followers",
"annual_report.summary.followers.total": "{count} total",
"annual_report.summary.here_it_is": "Here is your {year} in review:",
"annual_report.summary.highlighted_post.by_favourites": "most favourited post",
"annual_report.summary.highlighted_post.by_reblogs": "most boosted post",
"annual_report.summary.highlighted_post.by_replies": "post with the most replies",
"annual_report.summary.highlighted_post.possessive": "{name}'s",
"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.new_posts.new_posts": "new posts",
"annual_report.summary.percentile.text": "<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of Mastodon users.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.",
"annual_report.summary.thanks": "Thanks for being part of Mastodon!",
"attachments_list.unprocessed": "(unprocessed)",
"audio.hide": "Hide audio",
"block_modal.remote_users_caveat": "We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.",
@ -508,6 +526,8 @@
"notification.admin.report_statuses_other": "{name} reported {target}",
"notification.admin.sign_up": "{name} signed up",
"notification.admin.sign_up.name_and_others": "{name} and {count, plural, one {# other} other {# others}} signed up",
"notification.annual_report.message": "Your {year} #Wrapstodon awaits! Unveil your year's highlights and memorable moments on Mastodon!",
"notification.annual_report.view": "View #Wrapstodon",
"notification.favourite": "{name} favorited your post",
"notification.favourite.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your post",
"notification.follow": "{name} followed you",

View file

@ -0,0 +1,44 @@
export interface Percentiles {
followers: number;
statuses: number;
}
export interface NameAndCount {
name: string;
count: number;
}
export interface TimeSeriesMonth {
month: number;
statuses: number;
following: number;
followers: number;
}
export interface TopStatuses {
by_reblogs: number;
by_favourites: number;
by_replies: number;
}
export type Archetype =
| 'lurker'
| 'booster'
| 'pollster'
| 'replier'
| 'oracle';
interface AnnualReportV1 {
most_used_apps: NameAndCount[];
percentiles: Percentiles;
top_hashtags: NameAndCount[];
top_statuses: TopStatuses;
time_series: TimeSeriesMonth[];
archetype: Archetype;
}
export interface AnnualReport {
year: number;
schema_version: number;
data: AnnualReportV1;
}

View file

@ -1,6 +1,7 @@
import type {
ApiAccountRelationshipSeveranceEventJSON,
ApiAccountWarningJSON,
ApiAnnualReportEventJSON,
BaseNotificationGroupJSON,
ApiNotificationGroupJSON,
ApiNotificationJSON,
@ -65,6 +66,12 @@ export interface NotificationGroupSeveredRelationships
event: AccountRelationshipSeveranceEvent;
}
type AnnualReportEvent = ApiAnnualReportEventJSON;
export interface NotificationGroupAnnualReport
extends BaseNotification<'annual_report'> {
annualReport: AnnualReportEvent;
}
interface Report extends Omit<ApiReportJSON, 'target_account'> {
targetAccountId: string;
}
@ -86,7 +93,8 @@ export type NotificationGroup =
| NotificationGroupModerationWarning
| NotificationGroupSeveredRelationships
| NotificationGroupAdminSignUp
| NotificationGroupAdminReport;
| NotificationGroupAdminReport
| NotificationGroupAnnualReport;
function createReportFromJSON(reportJSON: ApiReportJSON): Report {
const { target_account, ...report } = reportJSON;
@ -112,6 +120,12 @@ function createAccountRelationshipSeveranceEventFromJSON(
return eventJson;
}
function createAnnualReportEventFromJSON(
eventJson: ApiAnnualReportEventJSON,
): AnnualReportEvent {
return eventJson;
}
export function createNotificationGroupFromJSON(
groupJson: ApiNotificationGroupJSON,
): NotificationGroup {
@ -145,7 +159,6 @@ export function createNotificationGroupFromJSON(
event: createAccountRelationshipSeveranceEventFromJSON(group.event),
sampleAccountIds,
};
case 'moderation_warning': {
const { moderation_warning, ...groupWithoutModerationWarning } = group;
return {
@ -154,6 +167,14 @@ export function createNotificationGroupFromJSON(
sampleAccountIds,
};
}
case 'annual_report': {
const { annual_report, ...groupWithoutAnnualReport } = group;
return {
...groupWithoutAnnualReport,
annualReport: createAnnualReportEventFromJSON(annual_report),
sampleAccountIds,
};
}
default:
return {
sampleAccountIds,