mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-27 18:59:15 -05:00
Redesign: Move server announcements to notifications page (#40674)
This commit is contained in:
72
app/javascript/mastodon/features/announcements/hooks.ts
Normal file
72
app/javascript/mastodon/features/announcements/hooks.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type {
|
||||
List as ImmutableList,
|
||||
Record as ImmutableRecord,
|
||||
} from 'immutable';
|
||||
|
||||
import { fetchAnnouncements } from '@/mastodon/actions/announcements';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
|
||||
type AnnouncementItem = ImmutableRecord<{
|
||||
id: string;
|
||||
starts_at: string;
|
||||
published_at: string;
|
||||
read: boolean;
|
||||
}>;
|
||||
|
||||
type AnnouncementsState = ImmutableRecord<{
|
||||
items: ImmutableList<AnnouncementItem>;
|
||||
isLoading: boolean;
|
||||
show: boolean;
|
||||
}>;
|
||||
|
||||
export function useHasAnnouncements({
|
||||
fetch = true,
|
||||
}: { fetch?: boolean } = {}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const hasAnnouncements = useAppSelector(
|
||||
(state) =>
|
||||
!(state.announcements as AnnouncementsState).get('items').isEmpty(),
|
||||
);
|
||||
const shouldShowAnnouncements = useAppSelector((state) =>
|
||||
(state.announcements as AnnouncementsState).get('show'),
|
||||
);
|
||||
const unreadAnnouncementCount = useAppSelector((state) =>
|
||||
(state.announcements as AnnouncementsState)
|
||||
.get('items')
|
||||
.count((item) => !item.get('read')),
|
||||
);
|
||||
const hasUnreadAnnouncements = !!unreadAnnouncementCount;
|
||||
|
||||
// Announcements display is transient – it's toggled on when
|
||||
// there are unread announcements or the `show` Redux state is
|
||||
// enabled, but resets to hidden when the page is left (by virtue
|
||||
// of React simply losing this state).
|
||||
const [showAnnouncements, setShowAnnouncements] = useState(false);
|
||||
if (
|
||||
(hasUnreadAnnouncements || shouldShowAnnouncements) &&
|
||||
!showAnnouncements
|
||||
) {
|
||||
setShowAnnouncements(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (fetch) {
|
||||
dispatch(fetchAnnouncements());
|
||||
}
|
||||
}, 700);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [fetch, dispatch]);
|
||||
|
||||
return {
|
||||
hasAnnouncements,
|
||||
hasUnreadAnnouncements,
|
||||
showAnnouncements,
|
||||
unreadAnnouncementCount,
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,7 @@ 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 { withBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint';
|
||||
import { Announcements } from '@/mastodon/features/announcements';
|
||||
|
||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
import { expandHomeTimeline } from '../../actions/timelines';
|
||||
@@ -26,7 +27,6 @@ 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';
|
||||
import { isRedesignEnabled } from '@/mastodon/utils/environment';
|
||||
import { ColumnHeader } from '@/mastodon/components/column_header';
|
||||
@@ -83,7 +83,9 @@ class HomeTimeline extends PureComponent {
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
setTimeout(() => this.props.dispatch(fetchAnnouncements()), 700);
|
||||
if (!isRedesignEnabled()) {
|
||||
setTimeout(() => this.props.dispatch(fetchAnnouncements()), 700);
|
||||
}
|
||||
this._checkIfReloadNeeded(false, this.props.isPartial);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ import { selectUnreadNotificationGroupsCount } from '@/mastodon/selectors/notifi
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import { invokeVirtualIosKeyboard } from '@/mastodon/utils/invoke_virtual_ios_keyboard';
|
||||
|
||||
import { useHasAnnouncements } from '../../announcements/hooks';
|
||||
|
||||
import { NavigationAccountCardAndMenu } from './account_card_and_menu';
|
||||
import { NavigationFooterLinks } from './footer_links';
|
||||
import { NavigationHeader } from './header';
|
||||
@@ -76,6 +78,15 @@ function useFollowedHashtags() {
|
||||
return { followedHashtags: tags };
|
||||
}
|
||||
|
||||
export function useNotificationsCount() {
|
||||
const unreadNotificationsCount = useAppSelector(
|
||||
selectUnreadNotificationGroupsCount,
|
||||
);
|
||||
const { unreadAnnouncementCount } = useHasAnnouncements();
|
||||
|
||||
return unreadNotificationsCount + unreadAnnouncementCount;
|
||||
}
|
||||
|
||||
const isFediverseFeedsLinkActive = (
|
||||
match: unknown,
|
||||
{ pathname }: { pathname: string },
|
||||
@@ -97,9 +108,7 @@ export const RedesignNavigationPanel: React.FC<{
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { signedIn } = useIdentity();
|
||||
const notificationsCount = useAppSelector(
|
||||
selectUnreadNotificationGroupsCount,
|
||||
);
|
||||
const notificationsCount = useNotificationsCount();
|
||||
|
||||
const openComposer = useCallback(() => {
|
||||
dispatch(closeNavigation());
|
||||
|
||||
@@ -23,11 +23,10 @@ import { FOCUS_TARGET } from '@/mastodon/components/navigation_focus_target';
|
||||
import { ComposeRedesignButton } from '@/mastodon/features/compose/redesign/trigger';
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useIdentity } from '@/mastodon/identity_context';
|
||||
import { selectUnreadNotificationGroupsCount } from '@/mastodon/selectors/notifications';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import { invokeVirtualIosKeyboard } from '@/mastodon/utils/invoke_virtual_ios_keyboard';
|
||||
|
||||
import { RedesignNavigationPanel } from '.';
|
||||
import { RedesignNavigationPanel, useNotificationsCount } from '.';
|
||||
import { AccountMenuItems } from './account_card_and_menu';
|
||||
import { LogoLockup } from './header';
|
||||
import classes from './mobile_nav.module.scss';
|
||||
@@ -38,9 +37,7 @@ export const RedesignMobileNavigation: React.FC = () => {
|
||||
const { accountId, signedIn } = useIdentity();
|
||||
const account = useAccount(accountId);
|
||||
|
||||
const notificationsCount = useAppSelector(
|
||||
selectUnreadNotificationGroupsCount,
|
||||
);
|
||||
const notificationsCount = useNotificationsCount();
|
||||
|
||||
const openMobileNav = useCallback(() => {
|
||||
dispatch(openNavigation());
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import DeleteForeverIcon from '@/material-icons/400-24px/delete_forever.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
export default class ClearColumnButton extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
return (
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.props.onClick}><Icon id='eraser' icon={DeleteForeverIcon} /> <FormattedMessage id='notifications.clear' defaultMessage='Clear notifications' /></button>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { TrashIcon } from '@phosphor-icons/react';
|
||||
|
||||
import { Button } from '@/mastodon/components/button/redesign';
|
||||
import { isRedesignEnabled } from '@/mastodon/utils/environment';
|
||||
import DeleteForeverIcon from '@/material-icons/400-24px/delete_forever.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
const ClearColumnButton: React.FC<{
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
}> = ({ onClick }) => {
|
||||
if (isRedesignEnabled()) {
|
||||
return (
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={onClick}
|
||||
leadingIcon={TrashIcon}
|
||||
color='destructive'
|
||||
variant='ghost'
|
||||
clipPadding
|
||||
>
|
||||
<FormattedMessage
|
||||
id='notifications.clear'
|
||||
defaultMessage='Clear notifications'
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='text-btn column-header__setting-btn'
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon id='eraser' icon={DeleteForeverIcon} />{' '}
|
||||
<FormattedMessage
|
||||
id='notifications.clear'
|
||||
defaultMessage='Clear notifications'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ClearColumnButton;
|
||||
@@ -13,6 +13,7 @@ import GrantPermissionButton from './grant_permission_button';
|
||||
import { PolicyControls } from './policy_controls';
|
||||
import SettingToggle from './setting_toggle';
|
||||
import { ColumnSettingsGroup } from './column_settings_group';
|
||||
import { ShowAnnouncementsButton } from './show_announcements_button';
|
||||
|
||||
class ColumnSettings extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -48,6 +49,7 @@ class ColumnSettings extends PureComponent {
|
||||
<div className='column-settings'>
|
||||
<section>
|
||||
<ClearColumnButton onClick={onClear} />
|
||||
<ShowAnnouncementsButton />
|
||||
</section>
|
||||
|
||||
{alertsEnabled && browserSupport && browserPermission === 'denied' && (
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { NewspaperIcon } from '@phosphor-icons/react';
|
||||
|
||||
import { toggleShowAnnouncements } from '@/mastodon/actions/announcements';
|
||||
import { closeModal } from '@/mastodon/actions/modal';
|
||||
import { Button } from '@/mastodon/components/button/redesign';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
import { isRedesignEnabled } from '@/mastodon/utils/environment';
|
||||
|
||||
import { useHasAnnouncements } from '../../announcements/hooks';
|
||||
|
||||
export const ShowAnnouncementsButton: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { hasAnnouncements, showAnnouncements } = useHasAnnouncements({
|
||||
fetch: false,
|
||||
});
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(toggleShowAnnouncements());
|
||||
dispatch(
|
||||
closeModal({ modalType: 'NOTIFICATION_SETTINGS', ignoreFocus: false }),
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
if (!isRedesignEnabled() || !hasAnnouncements || showAnnouncements) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
size='sm'
|
||||
leadingIcon={NewspaperIcon}
|
||||
variant='ghost'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='notifications.show_server_announcements'
|
||||
defaultMessage='Show server announcements'
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -52,6 +52,8 @@ import {
|
||||
} from 'mastodon/selectors/settings';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { Announcements } from '../announcements';
|
||||
import { useHasAnnouncements } from '../announcements/hooks';
|
||||
import {
|
||||
FilteredNotificationsBanner,
|
||||
FilteredNotificationsIconButton,
|
||||
@@ -220,9 +222,12 @@ export const Notifications: React.FC<{
|
||||
);
|
||||
}, [notifications, isLoading, hasMore, lastReadId, handleLoadGap]);
|
||||
|
||||
const { showAnnouncements } = useHasAnnouncements();
|
||||
|
||||
const prepend = (
|
||||
<>
|
||||
{needsNotificationPermission && <NotificationsPermissionBanner />}
|
||||
{showAnnouncements && <Announcements />}
|
||||
<FilteredNotificationsBanner />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1204,6 +1204,7 @@
|
||||
"notifications.policy.filter_private_mentions_title": "Unsolicited private mentions",
|
||||
"notifications.policy.title": "Manage notifications from…",
|
||||
"notifications.settings": "Notification Settings",
|
||||
"notifications.show_server_announcements": "Show server announcements",
|
||||
"notifications_permission_banner.enable": "Enable desktop notifications",
|
||||
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
|
||||
"notifications_permission_banner.title": "Never miss a thing",
|
||||
|
||||
Reference in New Issue
Block a user