mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-14 16:28:59 +00:00
Wrapstodon: Allow dismissing banner (#37202)
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import { reinsertAnnualReport, TIMELINE_WRAPSTODON } from '@/mastodon/reducers/slices/annual_report';
|
|
||||||
import api, { getLinks } from 'mastodon/api';
|
import api, { getLinks } from 'mastodon/api';
|
||||||
import { compareId } from 'mastodon/compare_id';
|
import { compareId } from 'mastodon/compare_id';
|
||||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
||||||
@@ -32,7 +31,6 @@ export const TIMELINE_GAP = null;
|
|||||||
export const TIMELINE_NON_STATUS_MARKERS = [
|
export const TIMELINE_NON_STATUS_MARKERS = [
|
||||||
TIMELINE_GAP,
|
TIMELINE_GAP,
|
||||||
TIMELINE_SUGGESTIONS,
|
TIMELINE_SUGGESTIONS,
|
||||||
TIMELINE_WRAPSTODON,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const loadPending = timeline => ({
|
export const loadPending = timeline => ({
|
||||||
@@ -132,7 +130,6 @@ export function expandTimeline(timelineId, path, params = {}) {
|
|||||||
|
|
||||||
if (timelineId === 'home') {
|
if (timelineId === 'home') {
|
||||||
dispatch(submitMarkers());
|
dispatch(submitMarkers());
|
||||||
dispatch(reinsertAnnualReport())
|
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import type { PropsWithChildren } from 'react';
|
import type { FC, ReactNode } from 'react';
|
||||||
import { useCallback, useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
import { changeSetting } from 'mastodon/actions/settings';
|
|
||||||
import { bannerSettings } from 'mastodon/settings';
|
import { useDismissible } from '../hooks/useDismissible';
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
|
||||||
|
|
||||||
import { IconButton } from './icon_button';
|
import { IconButton } from './icon_button';
|
||||||
|
|
||||||
@@ -16,48 +14,12 @@ const messages = defineMessages({
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDismissableBannerState({ id }: Props) {
|
export const DismissableBanner: FC<Props> = ({ id, children }) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
const dismissed: boolean = useAppSelector((state) =>
|
|
||||||
/* eslint-disable-next-line */
|
|
||||||
state.settings.getIn(['dismissed_banners', id], false),
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isVisible, setIsVisible] = useState(
|
|
||||||
!bannerSettings.get(id) && !dismissed,
|
|
||||||
);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const dismiss = useCallback(() => {
|
|
||||||
setIsVisible(false);
|
|
||||||
bannerSettings.set(id, true);
|
|
||||||
dispatch(changeSetting(['dismissed_banners', id], true));
|
|
||||||
}, [id, dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Store legacy localStorage setting on server
|
|
||||||
if (!isVisible && !dismissed) {
|
|
||||||
dispatch(changeSetting(['dismissed_banners', id], true));
|
|
||||||
}
|
|
||||||
}, [id, dispatch, isVisible, dismissed]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
wasDismissed: !isVisible,
|
|
||||||
dismiss,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
|
||||||
id,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { wasDismissed, dismiss } = useDismissableBannerState({
|
const { wasDismissed, dismiss } = useDismissible(id);
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (wasDismissed) {
|
if (wasDismissed) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
import Overlay from 'react-overlays/Overlay';
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
|
import { useDismissible } from '@/mastodon/hooks/useDismissible';
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
|
|
||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
import { useDismissableBannerState } from '../dismissable_banner';
|
|
||||||
import { Icon } from '../icon';
|
import { Icon } from '../icon';
|
||||||
|
|
||||||
const DISMISSABLE_BANNER_ID = 'notifications/remove_quote_hint';
|
const DISMISSIBLE_BANNER_ID = 'notifications/remove_quote_hint';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We don't want to show this hint in the UI more than once,
|
* We don't want to show this hint in the UI more than once,
|
||||||
@@ -29,9 +29,7 @@ export const RemoveQuoteHint: React.FC<{
|
|||||||
const anchorRef = useRef<HTMLDivElement>(null);
|
const anchorRef = useRef<HTMLDivElement>(null);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const { wasDismissed, dismiss } = useDismissableBannerState({
|
const { wasDismissed, dismiss } = useDismissible(DISMISSIBLE_BANNER_ID);
|
||||||
id: DISMISSABLE_BANNER_ID,
|
|
||||||
});
|
|
||||||
|
|
||||||
const shouldShowHint = !wasDismissed && canShowHint;
|
const shouldShowHint = !wasDismissed && canShowHint;
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import { debounce } from 'lodash';
|
|||||||
import { TIMELINE_GAP, TIMELINE_SUGGESTIONS } from 'mastodon/actions/timelines';
|
import { TIMELINE_GAP, TIMELINE_SUGGESTIONS } from 'mastodon/actions/timelines';
|
||||||
import { RegenerationIndicator } from 'mastodon/components/regeneration_indicator';
|
import { RegenerationIndicator } from 'mastodon/components/regeneration_indicator';
|
||||||
import { InlineFollowSuggestions } from 'mastodon/features/home_timeline/components/inline_follow_suggestions';
|
import { InlineFollowSuggestions } from 'mastodon/features/home_timeline/components/inline_follow_suggestions';
|
||||||
import { AnnualReportTimeline } from 'mastodon/features/annual_report/timeline';
|
|
||||||
import { TIMELINE_WRAPSTODON } from '@/mastodon/reducers/slices/annual_report';
|
|
||||||
|
|
||||||
import { StatusQuoteManager } from '../components/status_quoted';
|
import { StatusQuoteManager } from '../components/status_quoted';
|
||||||
|
|
||||||
@@ -67,10 +65,6 @@ export default class StatusList extends ImmutablePureComponent {
|
|||||||
return (
|
return (
|
||||||
<InlineFollowSuggestions key={TIMELINE_SUGGESTIONS} />
|
<InlineFollowSuggestions key={TIMELINE_SUGGESTIONS} />
|
||||||
);
|
);
|
||||||
case TIMELINE_WRAPSTODON:
|
|
||||||
return (
|
|
||||||
<AnnualReportTimeline key={TIMELINE_WRAPSTODON} />
|
|
||||||
)
|
|
||||||
case TIMELINE_GAP:
|
case TIMELINE_GAP:
|
||||||
return (
|
return (
|
||||||
<LoadGap
|
<LoadGap
|
||||||
|
|||||||
@@ -1,38 +1,60 @@
|
|||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
import { fn } from 'storybook/test';
|
import { action } from 'storybook/actions';
|
||||||
|
|
||||||
|
import type { AnyFunction, OmitValueType } from '@/mastodon/utils/types';
|
||||||
|
|
||||||
|
import type { AnnualReportAnnouncementProps } from '.';
|
||||||
import { AnnualReportAnnouncement } from '.';
|
import { AnnualReportAnnouncement } from '.';
|
||||||
|
|
||||||
const meta = {
|
type Props = OmitValueType<
|
||||||
title: 'Components/AnnualReportAnnouncement',
|
// We can't use the name 'state' here because it's reserved for overriding Redux state.
|
||||||
component: AnnualReportAnnouncement,
|
Omit<AnnualReportAnnouncementProps, 'state'> & {
|
||||||
args: {
|
reportState: AnnualReportAnnouncementProps['state'];
|
||||||
hasData: false,
|
|
||||||
isLoading: false,
|
|
||||||
year: '2025',
|
|
||||||
onRequestBuild: fn(),
|
|
||||||
onOpen: fn(),
|
|
||||||
},
|
},
|
||||||
} satisfies Meta<typeof AnnualReportAnnouncement>;
|
AnyFunction // Remove any functions, as they can't meaningfully be controlled in Storybook.
|
||||||
|
>;
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/AnnualReport/Announcement',
|
||||||
|
args: {
|
||||||
|
reportState: 'eligible',
|
||||||
|
year: '2025',
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
reportState: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
options: ['eligible', 'generating', 'available'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render({ reportState, ...args }: Props) {
|
||||||
|
return (
|
||||||
|
<AnnualReportAnnouncement
|
||||||
|
state={reportState}
|
||||||
|
{...args}
|
||||||
|
onDismiss={action('dismissed announcement')}
|
||||||
|
onOpen={action('opened report modal')}
|
||||||
|
onRequestBuild={action('requested build')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} satisfies Meta<Props>;
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {};
|
||||||
render: (args) => <AnnualReportAnnouncement {...args} />,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Loading: Story = {
|
export const Loading: Story = {
|
||||||
args: {
|
args: {
|
||||||
isLoading: true,
|
reportState: 'generating',
|
||||||
},
|
},
|
||||||
render: Default.render,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithData: Story = {
|
export const WithData: Story = {
|
||||||
args: {
|
args: {
|
||||||
hasData: true,
|
reportState: 'available',
|
||||||
},
|
},
|
||||||
render: Default.render,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,33 +2,36 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import type { ApiAnnualReportState } from '@/mastodon/api/annual_report';
|
||||||
import { Button } from '@/mastodon/components/button';
|
import { Button } from '@/mastodon/components/button';
|
||||||
|
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
|
|
||||||
export const AnnualReportAnnouncement: React.FC<{
|
export interface AnnualReportAnnouncementProps {
|
||||||
year: string;
|
year: string;
|
||||||
hasData: boolean;
|
state: Exclude<ApiAnnualReportState, 'ineligible'>;
|
||||||
isLoading: boolean;
|
|
||||||
onRequestBuild: () => void;
|
onRequestBuild: () => void;
|
||||||
onOpen: () => void;
|
onOpen: () => void;
|
||||||
}> = ({ year, hasData, isLoading, onRequestBuild, onOpen }) => {
|
onDismiss: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AnnualReportAnnouncement: React.FC<
|
||||||
|
AnnualReportAnnouncementProps
|
||||||
|
> = ({ year, state, onRequestBuild, onOpen, onDismiss }) => {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('theme-dark', styles.wrapper)}>
|
<div className={classNames('theme-dark', styles.wrapper)}>
|
||||||
<h2>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='annual_report.announcement.title'
|
id='annual_report.announcement.title'
|
||||||
defaultMessage='Wrapstodon {year} has arrived'
|
defaultMessage='Wrapstodon {year} has arrived'
|
||||||
values={{ year }}
|
values={{ year }}
|
||||||
|
tagName='h2'
|
||||||
/>
|
/>
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='annual_report.announcement.description'
|
id='annual_report.announcement.description'
|
||||||
defaultMessage='Discover more about your engagement on Mastodon over the past year.'
|
defaultMessage='Discover more about your engagement on Mastodon over the past year.'
|
||||||
|
tagName='p'
|
||||||
/>
|
/>
|
||||||
</p>
|
{state === 'available' ? (
|
||||||
{hasData ? (
|
|
||||||
<Button onClick={onOpen}>
|
<Button onClick={onOpen}>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='annual_report.announcement.action_view'
|
id='annual_report.announcement.action_view'
|
||||||
@@ -36,13 +39,21 @@ export const AnnualReportAnnouncement: React.FC<{
|
|||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button loading={isLoading} onClick={onRequestBuild}>
|
<Button loading={state === 'generating'} onClick={onRequestBuild}>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='annual_report.announcement.action_build'
|
id='annual_report.announcement.action_build'
|
||||||
defaultMessage='Build my Wrapstodon'
|
defaultMessage='Build my Wrapstodon'
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{state === 'eligible' && (
|
||||||
|
<Button onClick={onDismiss} plain className={styles.closeButton}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='annual_report.announcement.action_dismiss'
|
||||||
|
defaultMessage='No thanks'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
radial-gradient(at 16% 95%, #1e948299 0, transparent 50%)
|
radial-gradient(at 16% 95%, #1e948299 0, transparent 50%)
|
||||||
var(--color-bg-primary);
|
var(--color-bg-primary);
|
||||||
border-bottom: 1px solid var(--color-border-primary);
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@@ -26,4 +27,11 @@
|
|||||||
p {
|
p {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
margin-inline: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
import { openModal } from '@/mastodon/actions/modal';
|
import { openModal } from '@/mastodon/actions/modal';
|
||||||
|
import { useDismissible } from '@/mastodon/hooks/useDismissible';
|
||||||
import {
|
import {
|
||||||
generateReport,
|
generateReport,
|
||||||
selectWrapstodonYear,
|
selectWrapstodonYear,
|
||||||
@@ -19,21 +20,26 @@ export const AnnualReportTimeline: FC = () => {
|
|||||||
void dispatch(generateReport());
|
void dispatch(generateReport());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const { wasDismissed, dismiss } = useDismissible(
|
||||||
|
`annual_report_announcement_${year}`,
|
||||||
|
);
|
||||||
|
|
||||||
const handleOpen = useCallback(() => {
|
const handleOpen = useCallback(() => {
|
||||||
dispatch(openModal({ modalType: 'ANNUAL_REPORT', modalProps: {} }));
|
dispatch(openModal({ modalType: 'ANNUAL_REPORT', modalProps: {} }));
|
||||||
}, [dispatch]);
|
dismiss();
|
||||||
|
}, [dismiss, dispatch]);
|
||||||
|
|
||||||
if (!year || !state || state === 'ineligible') {
|
if (!year || wasDismissed || !state || state === 'ineligible') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnnualReportAnnouncement
|
<AnnualReportAnnouncement
|
||||||
year={year.toString()}
|
year={year.toString()}
|
||||||
hasData={state === 'available'}
|
state={state}
|
||||||
isLoading={state === 'generating'}
|
|
||||||
onRequestBuild={handleBuildRequest}
|
onRequestBuild={handleBuildRequest}
|
||||||
onOpen={handleOpen}
|
onOpen={handleOpen}
|
||||||
|
onDismiss={dismiss}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export const CriticalUpdateBanner = () => (
|
import { criticalUpdatesPending } from '@/mastodon/initial_state';
|
||||||
|
|
||||||
|
export const CriticalUpdateBanner: FC = () => {
|
||||||
|
if (!criticalUpdatesPending) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
<div className='warning-banner'>
|
<div className='warning-banner'>
|
||||||
<div className='warning-banner__message'>
|
<div className='warning-banner__message'>
|
||||||
<h1>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='home.pending_critical_update.title'
|
id='home.pending_critical_update.title'
|
||||||
defaultMessage='Critical security update available!'
|
defaultMessage='Critical security update available!'
|
||||||
|
tagName='h1'
|
||||||
/>
|
/>
|
||||||
</h1>
|
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='home.pending_critical_update.body'
|
id='home.pending_critical_update.body'
|
||||||
@@ -23,4 +30,5 @@ export const CriticalUpdateBanner = () => (
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/an
|
|||||||
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
||||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||||
import { criticalUpdatesPending } from 'mastodon/initial_state';
|
|
||||||
import { withBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint';
|
import { withBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint';
|
||||||
|
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
@@ -27,6 +26,7 @@ import StatusListContainer from '../ui/containers/status_list_container';
|
|||||||
import { ColumnSettings } from './components/column_settings';
|
import { ColumnSettings } from './components/column_settings';
|
||||||
import { CriticalUpdateBanner } from './components/critical_update_banner';
|
import { CriticalUpdateBanner } from './components/critical_update_banner';
|
||||||
import { Announcements } from './components/announcements';
|
import { Announcements } from './components/announcements';
|
||||||
|
import { AnnualReportTimeline } from '../annual_report/timeline';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.home', defaultMessage: 'Home' },
|
title: { id: 'column.home', defaultMessage: 'Home' },
|
||||||
@@ -127,7 +127,10 @@ class HomeTimeline extends PureComponent {
|
|||||||
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements, matchesBreakpoint } = this.props;
|
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements, matchesBreakpoint } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
const { signedIn } = this.props.identity;
|
const { signedIn } = this.props.identity;
|
||||||
const banners = [];
|
const banners = [
|
||||||
|
<CriticalUpdateBanner key='critical-update-banner' />,
|
||||||
|
<AnnualReportTimeline key='annual-report' />
|
||||||
|
];
|
||||||
|
|
||||||
let announcementsButton;
|
let announcementsButton;
|
||||||
|
|
||||||
@@ -145,10 +148,6 @@ class HomeTimeline extends PureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (criticalUpdatesPending) {
|
|
||||||
banners.push(<CriticalUpdateBanner key='critical-update-banner' />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
|
|||||||
42
app/javascript/mastodon/hooks/useDismissible.ts
Normal file
42
app/javascript/mastodon/hooks/useDismissible.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import { changeSetting } from '@/mastodon/actions/settings';
|
||||||
|
import { bannerSettings } from '@/mastodon/settings';
|
||||||
|
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
|
||||||
|
|
||||||
|
export function useDismissible(id: string) {
|
||||||
|
// We use "dismissed_banners" as that was what this was previously called,
|
||||||
|
// but we can use this to track any dismissible state.
|
||||||
|
const dismissed = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
!!(
|
||||||
|
state.settings as ImmutableMap<
|
||||||
|
'dismissed_banners',
|
||||||
|
ImmutableMap<string, boolean>
|
||||||
|
>
|
||||||
|
).getIn(['dismissed_banners', id], false),
|
||||||
|
);
|
||||||
|
|
||||||
|
const wasDismissed = !!bannerSettings.get(id) || dismissed;
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const dismiss = useCallback(() => {
|
||||||
|
bannerSettings.set(id, true);
|
||||||
|
dispatch(changeSetting(['dismissed_banners', id], true));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Store legacy localStorage setting on server
|
||||||
|
if (wasDismissed && !dismissed) {
|
||||||
|
dispatch(changeSetting(['dismissed_banners', id], true));
|
||||||
|
}
|
||||||
|
}, [id, dispatch, wasDismissed, dismissed]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
wasDismissed,
|
||||||
|
dismiss,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -114,6 +114,7 @@
|
|||||||
"alt_text_modal.done": "Done",
|
"alt_text_modal.done": "Done",
|
||||||
"announcement.announcement": "Announcement",
|
"announcement.announcement": "Announcement",
|
||||||
"annual_report.announcement.action_build": "Build my Wrapstodon",
|
"annual_report.announcement.action_build": "Build my Wrapstodon",
|
||||||
|
"annual_report.announcement.action_dismiss": "No thanks",
|
||||||
"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",
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import {
|
|||||||
importFetchedAccounts,
|
importFetchedAccounts,
|
||||||
importFetchedStatuses,
|
importFetchedStatuses,
|
||||||
} from '@/mastodon/actions/importer';
|
} from '@/mastodon/actions/importer';
|
||||||
import { insertIntoTimeline } from '@/mastodon/actions/timelines';
|
|
||||||
import { timelineDelete } from '@/mastodon/actions/timelines_typed';
|
|
||||||
import type { ApiAnnualReportState } from '@/mastodon/api/annual_report';
|
import type { ApiAnnualReportState } from '@/mastodon/api/annual_report';
|
||||||
import {
|
import {
|
||||||
apiGetAnnualReport,
|
apiGetAnnualReport,
|
||||||
@@ -21,8 +19,6 @@ import {
|
|||||||
createDataLoadingThunk,
|
createDataLoadingThunk,
|
||||||
} from '../../store/typed_functions';
|
} from '../../store/typed_functions';
|
||||||
|
|
||||||
export const TIMELINE_WRAPSTODON = 'inline-wrapstodon';
|
|
||||||
|
|
||||||
interface AnnualReportState {
|
interface AnnualReportState {
|
||||||
state?: ApiAnnualReportState;
|
state?: ApiAnnualReportState;
|
||||||
report?: AnnualReport;
|
report?: AnnualReport;
|
||||||
@@ -64,37 +60,12 @@ export const selectWrapstodonYear = createAppSelector(
|
|||||||
// This kicks everything off, and is called after fetching the server info.
|
// This kicks everything off, and is called after fetching the server info.
|
||||||
export const checkAnnualReport = createAppThunk(
|
export const checkAnnualReport = createAppThunk(
|
||||||
`${annualReportSlice.name}/checkAnnualReport`,
|
`${annualReportSlice.name}/checkAnnualReport`,
|
||||||
async (_arg: unknown, { dispatch, getState }) => {
|
(_arg: unknown, { dispatch, getState }) => {
|
||||||
const year = selectWrapstodonYear(getState());
|
const year = selectWrapstodonYear(getState());
|
||||||
if (!year) {
|
if (!year) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const state = await dispatch(fetchReportState());
|
void dispatch(fetchReportState());
|
||||||
if (
|
|
||||||
state.meta.requestStatus === 'fulfilled' &&
|
|
||||||
state.payload !== 'ineligible'
|
|
||||||
) {
|
|
||||||
dispatch(insertIntoTimeline('home', TIMELINE_WRAPSTODON, 1));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const reinsertAnnualReport = createAppThunk(
|
|
||||||
`${annualReportSlice.name}/reinsertAnnualReport`,
|
|
||||||
(_arg: unknown, { dispatch, getState }) => {
|
|
||||||
dispatch(
|
|
||||||
timelineDelete({
|
|
||||||
statusId: TIMELINE_WRAPSTODON,
|
|
||||||
accountId: '',
|
|
||||||
references: [],
|
|
||||||
reblogOf: null,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const { state } = getState().annualReport;
|
|
||||||
if (!state || state === 'ineligible') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch(insertIntoTimeline('home', TIMELINE_WRAPSTODON, 1));
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -14,3 +14,9 @@
|
|||||||
export type SomeRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
export type SomeRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
||||||
export type SomeOptional<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> &
|
export type SomeOptional<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> &
|
||||||
Partial<Pick<T, K>>;
|
Partial<Pick<T, K>>;
|
||||||
|
|
||||||
|
export type OmitValueType<T, V> = {
|
||||||
|
[K in keyof T as T[K] extends V ? never : K]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnyFunction = (...args: never) => unknown;
|
||||||
|
|||||||
Reference in New Issue
Block a user