Merge commit '4af8e83c8a236265c46a1b984fa5dbd3a7c73dfe' into glitch-soc/merge-upstream
This commit is contained in:
+40
-18
@@ -1,38 +1,60 @@
|
||||
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 '.';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/AnnualReportAnnouncement',
|
||||
component: AnnualReportAnnouncement,
|
||||
args: {
|
||||
hasData: false,
|
||||
isLoading: false,
|
||||
year: '2025',
|
||||
onRequestBuild: fn(),
|
||||
onOpen: fn(),
|
||||
type Props = OmitValueType<
|
||||
// We can't use the name 'state' here because it's reserved for overriding Redux state.
|
||||
Omit<AnnualReportAnnouncementProps, 'state'> & {
|
||||
reportState: AnnualReportAnnouncementProps['state'];
|
||||
},
|
||||
} 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;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => <AnnualReportAnnouncement {...args} />,
|
||||
};
|
||||
export const Default: Story = {};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
isLoading: true,
|
||||
reportState: 'generating',
|
||||
},
|
||||
render: Default.render,
|
||||
};
|
||||
|
||||
export const WithData: Story = {
|
||||
args: {
|
||||
hasData: true,
|
||||
reportState: 'available',
|
||||
},
|
||||
render: Default.render,
|
||||
};
|
||||
|
||||
@@ -2,33 +2,36 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { ApiAnnualReportState } from '@/mastodon/api/annual_report';
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
export const AnnualReportAnnouncement: React.FC<{
|
||||
export interface AnnualReportAnnouncementProps {
|
||||
year: string;
|
||||
hasData: boolean;
|
||||
isLoading: boolean;
|
||||
state: Exclude<ApiAnnualReportState, 'ineligible'>;
|
||||
onRequestBuild: () => void;
|
||||
onOpen: () => void;
|
||||
}> = ({ year, hasData, isLoading, onRequestBuild, onOpen }) => {
|
||||
onOpen?: () => void; // This is optional when inside the modal, as it won't be shown then.
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export const AnnualReportAnnouncement: React.FC<
|
||||
AnnualReportAnnouncementProps
|
||||
> = ({ year, state, onRequestBuild, onOpen, onDismiss }) => {
|
||||
return (
|
||||
<div className={classNames('theme-dark', styles.wrapper)}>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id='annual_report.announcement.title'
|
||||
defaultMessage='Wrapstodon {year} has arrived'
|
||||
values={{ year }}
|
||||
/>
|
||||
</h2>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='annual_report.announcement.description'
|
||||
defaultMessage='Discover more about your engagement on Mastodon over the past year.'
|
||||
/>
|
||||
</p>
|
||||
{hasData ? (
|
||||
<FormattedMessage
|
||||
id='annual_report.announcement.title'
|
||||
defaultMessage='Wrapstodon {year} has arrived'
|
||||
values={{ year }}
|
||||
tagName='h2'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='annual_report.announcement.description'
|
||||
defaultMessage='Discover more about your engagement on Mastodon over the past year.'
|
||||
tagName='p'
|
||||
/>
|
||||
{state === 'available' ? (
|
||||
<Button onClick={onOpen}>
|
||||
<FormattedMessage
|
||||
id='annual_report.announcement.action_view'
|
||||
@@ -36,13 +39,21 @@ export const AnnualReportAnnouncement: React.FC<{
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
<Button loading={isLoading} onClick={onRequestBuild}>
|
||||
<Button loading={state === 'generating'} onClick={onRequestBuild}>
|
||||
<FormattedMessage
|
||||
id='annual_report.announcement.action_build'
|
||||
defaultMessage='Build my Wrapstodon'
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{state === 'eligible' && (
|
||||
<Button onClick={onDismiss} plain className={styles.closeButton}>
|
||||
<FormattedMessage
|
||||
id='annual_report.announcement.action_dismiss'
|
||||
defaultMessage='No thanks'
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
radial-gradient(at 16% 95%, #1e948299 0, transparent 50%)
|
||||
var(--color-bg-primary);
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
position: relative;
|
||||
pointer-events: all;
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
@@ -26,4 +28,15 @@
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
:global(.modal-root__modal) & {
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ import replier from '@/images/archetypes/replier.png';
|
||||
import space_elements from '@/images/archetypes/space_elements.png';
|
||||
import { Avatar } from '@/mastodon/components/avatar';
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import type {
|
||||
AnnualReport,
|
||||
Archetype as ArchetypeData,
|
||||
} from '@/mastodon/models/annual_report';
|
||||
import { wrapstodonSettings } from '@/mastodon/settings';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
import { ShareButton } from './share_button';
|
||||
@@ -117,9 +119,16 @@ export const Archetype: React.FC<{
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const isSelfView = context === 'modal';
|
||||
|
||||
const [isRevealed, setIsRevealed] = useState(!isSelfView);
|
||||
const [isRevealed, setIsRevealed] = useState(
|
||||
() =>
|
||||
!isSelfView ||
|
||||
(me ? (wrapstodonSettings.get(me)?.archetypeRevealed ?? false) : true),
|
||||
);
|
||||
const reveal = useCallback(() => {
|
||||
setIsRevealed(true);
|
||||
if (me) {
|
||||
wrapstodonSettings.set(me, { archetypeRevealed: true });
|
||||
}
|
||||
wrapperRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
@@ -128,7 +137,8 @@ export const Archetype: React.FC<{
|
||||
? archetypeSelfDescriptions
|
||||
: archetypePublicDescriptions;
|
||||
|
||||
const name = account?.display_name;
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we specifically want to fallback if `display_name` is empty
|
||||
const name = account?.display_name || account?.username;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -174,7 +174,7 @@ $mobile-breakpoint: 540px;
|
||||
|
||||
.title {
|
||||
text-transform: uppercase;
|
||||
color: #c2c8ff;
|
||||
color: var(--color-text-brand-soft);
|
||||
font-weight: 500;
|
||||
|
||||
&:last-child {
|
||||
@@ -332,3 +332,20 @@ $mobile-breakpoint: 540px;
|
||||
left: 0;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.shareButtonWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.secondaryShareButton {
|
||||
// Extra selector is needed to override color
|
||||
&:global(.button) {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.navItemBadge {
|
||||
background: var(--color-bg-brand-soft);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,11 @@ import { closeModal } from '@/mastodon/actions/modal';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import {
|
||||
createAppSelector,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '@/mastodon/store';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
import { Archetype } from './archetype';
|
||||
@@ -23,21 +27,26 @@ import { NewPosts } from './new_posts';
|
||||
|
||||
const moduleClassNames = classNames.bind(styles);
|
||||
|
||||
const accountSelector = createAppSelector(
|
||||
[(state) => state.accounts, (state) => state.annualReport.report],
|
||||
(accounts, report) => {
|
||||
if (me) {
|
||||
return accounts.get(me);
|
||||
}
|
||||
if (report?.schema_version === 2) {
|
||||
return accounts.get(report.account_id);
|
||||
}
|
||||
return 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 account = useAppSelector(accountSelector);
|
||||
|
||||
const close = useCallback(() => {
|
||||
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import type { MouseEventHandler } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { closeModal } from '@/mastodon/actions/modal';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
import {
|
||||
generateReport,
|
||||
selectWrapstodonYear,
|
||||
} from '@/mastodon/reducers/slices/annual_report';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
|
||||
import { AnnualReport } from '.';
|
||||
import { AnnualReportAnnouncement } from './announcement';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const AnnualReportModal: React.FC<{
|
||||
@@ -15,17 +21,42 @@ const AnnualReportModal: React.FC<{
|
||||
onChangeBackgroundColor('var(--color-bg-media-base)');
|
||||
}, [onChangeBackgroundColor]);
|
||||
|
||||
const { state } = useAppSelector((state) => state.annualReport);
|
||||
const year = useAppSelector(selectWrapstodonYear);
|
||||
|
||||
const showAnnouncement = year && state && state !== 'available';
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleCloseModal = useCallback<React.MouseEventHandler<HTMLDivElement>>(
|
||||
|
||||
const handleBuildRequest = useCallback(() => {
|
||||
void dispatch(generateReport());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleCloseModal: MouseEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (e.target === e.currentTarget)
|
||||
dispatch(
|
||||
closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }),
|
||||
);
|
||||
if (e.target === e.currentTarget) {
|
||||
handleClose();
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
[handleClose],
|
||||
);
|
||||
|
||||
// Auto-close if ineligible
|
||||
useEffect(() => {
|
||||
if (state === 'ineligible') {
|
||||
handleClose();
|
||||
}
|
||||
}, [handleClose, state]);
|
||||
|
||||
if (state === 'ineligible') {
|
||||
// Not sure how you got here, but don't show anything.
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
// It's fine not to provide a keyboard handler here since there is a global
|
||||
// [Esc] key listener that will close open modals.
|
||||
@@ -40,7 +71,16 @@ const AnnualReportModal: React.FC<{
|
||||
)}
|
||||
onClick={handleCloseModal}
|
||||
>
|
||||
<AnnualReport context='modal' />
|
||||
{!showAnnouncement ? (
|
||||
<AnnualReport context='modal' />
|
||||
) : (
|
||||
<AnnualReportAnnouncement
|
||||
year={year.toString()}
|
||||
state={state}
|
||||
onDismiss={handleClose}
|
||||
onRequestBuild={handleBuildRequest}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import IconPlanet from '@/images/icons/icon_planet.svg?react';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { Icon } from '@/mastodon/components/icon';
|
||||
import { selectWrapstodonYear } from '@/mastodon/reducers/slices/annual_report';
|
||||
import {
|
||||
createAppSelector,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '@/mastodon/store';
|
||||
|
||||
import classes from './index.module.scss';
|
||||
|
||||
const selectReportModalOpen = createAppSelector(
|
||||
[(state) => state.modal.getIn(['stack', 0, 'modalType'])],
|
||||
(modalType) => modalType === 'ANNUAL_REPORT',
|
||||
);
|
||||
|
||||
export const AnnualReportNavItem: FC = () => {
|
||||
const { state } = useAppSelector((state) => state.annualReport);
|
||||
const year = useAppSelector(selectWrapstodonYear);
|
||||
const active = useAppSelector(selectReportModalOpen);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(openModal({ modalType: 'ANNUAL_REPORT', modalProps: {} }));
|
||||
}, [dispatch]);
|
||||
|
||||
if (!year || !state || state === 'ineligible') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className={classNames('column-link column-link--transparent', { active })}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Icon icon={IconPlanet} id='wrapstodon-planet' width='24' height='24' />
|
||||
<span>Wrapstodon {year}</span>
|
||||
<span className={classNames('column-link__badge', classes.navItemBadge)}>
|
||||
<FormattedMessage
|
||||
id='annual_report.nav_item.badge'
|
||||
defaultMessage='New'
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import type { FC } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { showAlert } from '@/mastodon/actions/alerts';
|
||||
import { resetCompose, focusCompose } from '@/mastodon/actions/compose';
|
||||
import { closeModal } from '@/mastodon/actions/modal';
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
@@ -10,6 +11,7 @@ import type { AnnualReport as AnnualReportData } from '@/mastodon/models/annual_
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
import { archetypeNames } from './archetype';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
share_message: {
|
||||
@@ -20,11 +22,24 @@ const messages = defineMessages({
|
||||
id: 'annual_report.summary.share_on_mastodon',
|
||||
defaultMessage: 'Share on Mastodon',
|
||||
},
|
||||
share_elsewhere: {
|
||||
id: 'annual_report.summary.share_elsewhere',
|
||||
defaultMessage: 'Share elsewhere',
|
||||
},
|
||||
copy_link: {
|
||||
id: 'annual_report.summary.copy_link',
|
||||
defaultMessage: 'Copy link',
|
||||
},
|
||||
copied: {
|
||||
id: 'copy_icon_button.copied',
|
||||
defaultMessage: 'Copied to clipboard',
|
||||
},
|
||||
});
|
||||
|
||||
export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleShareClick = useCallback(() => {
|
||||
// Generate the share message.
|
||||
const archetypeName = intl.formatMessage(
|
||||
@@ -47,10 +62,35 @@ export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => {
|
||||
dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }));
|
||||
}, [report, intl, dispatch]);
|
||||
|
||||
const supportsNativeShare = 'share' in navigator;
|
||||
|
||||
const handleSecondaryShare = useCallback(() => {
|
||||
if (report.schema_version === 2 && report.share_url) {
|
||||
if (supportsNativeShare) {
|
||||
void navigator.share({
|
||||
url: report.share_url,
|
||||
});
|
||||
} else {
|
||||
void navigator.clipboard.writeText(report.share_url);
|
||||
dispatch(showAlert({ message: messages.copied }));
|
||||
}
|
||||
}
|
||||
}, [report, supportsNativeShare, dispatch]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
text={intl.formatMessage(messages.share_on_mastodon)}
|
||||
onClick={handleShareClick}
|
||||
/>
|
||||
<div className={styles.shareButtonWrapper}>
|
||||
<Button
|
||||
text={intl.formatMessage(messages.share_on_mastodon)}
|
||||
onClick={handleShareClick}
|
||||
/>
|
||||
<Button
|
||||
plain
|
||||
className={styles.secondaryShareButton}
|
||||
text={intl.formatMessage(
|
||||
supportsNativeShare ? messages.share_elsewhere : messages.copy_link,
|
||||
)}
|
||||
onClick={handleSecondaryShare}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,14 +17,11 @@ export const WrapstodonSharedPage: FC = () => {
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.footer'
|
||||
defaultMessage='Generated with {heart} by the Mastodon team'
|
||||
values={{ heart: '♥' }}
|
||||
values={{ heart: '🐘' }}
|
||||
/>
|
||||
<nav className={classes.nav}>
|
||||
<a href='/about'>
|
||||
<FormattedMessage
|
||||
id='footer.about_this_server'
|
||||
defaultMessage='About'
|
||||
/>
|
||||
<a href='https://joinmastodon.org'>
|
||||
<FormattedMessage id='footer.about' defaultMessage='About' />
|
||||
</a>
|
||||
{!me && (
|
||||
<a href='https://joinmastodon.org/servers'>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { useDismissible } from '@/mastodon/hooks/useDismissible';
|
||||
import {
|
||||
generateReport,
|
||||
selectWrapstodonYear,
|
||||
@@ -19,21 +20,26 @@ export const AnnualReportTimeline: FC = () => {
|
||||
void dispatch(generateReport());
|
||||
}, [dispatch]);
|
||||
|
||||
const { wasDismissed, dismiss } = useDismissible(
|
||||
`annual_report_announcement_${year}`,
|
||||
);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
dispatch(openModal({ modalType: 'ANNUAL_REPORT', modalProps: {} }));
|
||||
}, [dispatch]);
|
||||
dismiss();
|
||||
}, [dismiss, dispatch]);
|
||||
|
||||
if (!year || !state || state === 'ineligible') {
|
||||
if (!year || wasDismissed || !state || state === 'ineligible') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnnualReportAnnouncement
|
||||
year={year.toString()}
|
||||
hasData={state === 'available'}
|
||||
isLoading={state === 'generating'}
|
||||
state={state}
|
||||
onRequestBuild={handleBuildRequest}
|
||||
onOpen={handleOpen}
|
||||
onDismiss={dismiss}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -41,8 +41,8 @@ const persistVolume = (volume: number, muted: boolean) => {
|
||||
};
|
||||
|
||||
const restoreVolume = (audio: HTMLAudioElement) => {
|
||||
const volume = (playerSettings.get('volume') as number | undefined) ?? 0.5;
|
||||
const muted = (playerSettings.get('muted') as boolean | undefined) ?? false;
|
||||
const volume = playerSettings.get('volume') ?? 0.5;
|
||||
const muted = playerSettings.get('muted') ?? false;
|
||||
|
||||
audio.volume = volume;
|
||||
audio.muted = muted;
|
||||
|
||||
+26
-18
@@ -1,26 +1,34 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export const CriticalUpdateBanner = () => (
|
||||
<div className='warning-banner'>
|
||||
<div className='warning-banner__message'>
|
||||
<h1>
|
||||
import { criticalUpdatesPending } from '@/mastodon/initial_state';
|
||||
|
||||
export const CriticalUpdateBanner: FC = () => {
|
||||
if (!criticalUpdatesPending) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className='warning-banner'>
|
||||
<div className='warning-banner__message'>
|
||||
<FormattedMessage
|
||||
id='home.pending_critical_update.title'
|
||||
defaultMessage='Critical security update available!'
|
||||
tagName='h1'
|
||||
/>
|
||||
</h1>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='home.pending_critical_update.body'
|
||||
defaultMessage='Please update your Mastodon server as soon as possible!'
|
||||
/>{' '}
|
||||
<a href='/admin/software_updates'>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='home.pending_critical_update.link'
|
||||
defaultMessage='See updates'
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
id='home.pending_critical_update.body'
|
||||
defaultMessage='Please update your Mastodon server as soon as possible!'
|
||||
/>{' '}
|
||||
<a href='/admin/software_updates'>
|
||||
<FormattedMessage
|
||||
id='home.pending_critical_update.link'
|
||||
defaultMessage='See updates'
|
||||
/>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/an
|
||||
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||
import { criticalUpdatesPending } from 'mastodon/initial_state';
|
||||
import { withBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint';
|
||||
|
||||
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 { CriticalUpdateBanner } from './components/critical_update_banner';
|
||||
import { Announcements } from './components/announcements';
|
||||
import { AnnualReportTimeline } from '../annual_report/timeline';
|
||||
|
||||
const messages = defineMessages({
|
||||
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 pinned = !!columnId;
|
||||
const { signedIn } = this.props.identity;
|
||||
const banners = [];
|
||||
const banners = [
|
||||
<CriticalUpdateBanner key='critical-update-banner' />,
|
||||
<AnnualReportTimeline key='annual-report' />
|
||||
];
|
||||
|
||||
let announcementsButton;
|
||||
|
||||
@@ -145,10 +148,6 @@ class HomeTimeline extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (criticalUpdatesPending) {
|
||||
banners.push(<CriticalUpdateBanner key='critical-update-banner' />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
|
||||
@@ -46,6 +46,8 @@ import { canViewFeed } from 'mastodon/permissions';
|
||||
import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { AnnualReportNavItem } from '../annual_report/nav_item';
|
||||
|
||||
import { DisabledAccountBanner } from './components/disabled_account_banner';
|
||||
import { FollowedTagsPanel } from './components/followed_tags_panel';
|
||||
import { ListPanel } from './components/list_panel';
|
||||
@@ -294,6 +296,8 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({
|
||||
|
||||
<FollowRequestsLink />
|
||||
|
||||
<AnnualReportNavItem />
|
||||
|
||||
<hr />
|
||||
|
||||
<ListPanel />
|
||||
|
||||
@@ -139,8 +139,8 @@ const persistVolume = (volume: number, muted: boolean) => {
|
||||
};
|
||||
|
||||
const restoreVolume = (video: HTMLVideoElement) => {
|
||||
const volume = (playerSettings.get('volume') as number | undefined) ?? 0.5;
|
||||
const muted = (playerSettings.get('muted') as boolean | undefined) ?? false;
|
||||
const volume = playerSettings.get('volume') ?? 0.5;
|
||||
const muted = playerSettings.get('muted') ?? false;
|
||||
|
||||
video.volume = volume;
|
||||
video.muted = muted;
|
||||
|
||||
Reference in New Issue
Block a user