diff --git a/app/javascript/mastodon/features/home_timeline/components/announcements/announcement.tsx b/app/javascript/mastodon/features/announcements/announcement.tsx similarity index 100% rename from app/javascript/mastodon/features/home_timeline/components/announcements/announcement.tsx rename to app/javascript/mastodon/features/announcements/announcement.tsx diff --git a/app/javascript/mastodon/features/announcements/hooks.ts b/app/javascript/mastodon/features/announcements/hooks.ts new file mode 100644 index 00000000000..ec9884bb611 --- /dev/null +++ b/app/javascript/mastodon/features/announcements/hooks.ts @@ -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; + 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, + }; +} diff --git a/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx b/app/javascript/mastodon/features/announcements/index.tsx similarity index 100% rename from app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx rename to app/javascript/mastodon/features/announcements/index.tsx diff --git a/app/javascript/mastodon/features/home_timeline/components/announcements/reactions.tsx b/app/javascript/mastodon/features/announcements/reactions.tsx similarity index 100% rename from app/javascript/mastodon/features/home_timeline/components/announcements/reactions.tsx rename to app/javascript/mastodon/features/announcements/reactions.tsx diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx index fd1c9d4b1c7..f6a31486643 100644 --- a/app/javascript/mastodon/features/home_timeline/index.jsx +++ b/app/javascript/mastodon/features/home_timeline/index.jsx @@ -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); } diff --git a/app/javascript/mastodon/features/navigation_panel/redesign/index.tsx b/app/javascript/mastodon/features/navigation_panel/redesign/index.tsx index b60157c6647..6f872a08621 100644 --- a/app/javascript/mastodon/features/navigation_panel/redesign/index.tsx +++ b/app/javascript/mastodon/features/navigation_panel/redesign/index.tsx @@ -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()); diff --git a/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.tsx b/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.tsx index 66c79cf8050..815eeda48a8 100644 --- a/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.tsx +++ b/app/javascript/mastodon/features/navigation_panel/redesign/mobile_nav.tsx @@ -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()); diff --git a/app/javascript/mastodon/features/notifications/components/clear_column_button.jsx b/app/javascript/mastodon/features/notifications/components/clear_column_button.jsx deleted file mode 100644 index 13e099e1aa1..00000000000 --- a/app/javascript/mastodon/features/notifications/components/clear_column_button.jsx +++ /dev/null @@ -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 ( - - ); - } - -} diff --git a/app/javascript/mastodon/features/notifications/components/clear_column_button.tsx b/app/javascript/mastodon/features/notifications/components/clear_column_button.tsx new file mode 100644 index 00000000000..2e2c038c8b7 --- /dev/null +++ b/app/javascript/mastodon/features/notifications/components/clear_column_button.tsx @@ -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; +}> = ({ onClick }) => { + if (isRedesignEnabled()) { + return ( + + ); + } + return ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default ClearColumnButton; diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index 588af106b58..e0d4e6ff73b 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -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 {
+
{alertsEnabled && browserSupport && browserPermission === 'denied' && ( diff --git a/app/javascript/mastodon/features/notifications/components/show_announcements_button.tsx b/app/javascript/mastodon/features/notifications/components/show_announcements_button.tsx new file mode 100644 index 00000000000..49cec4925dd --- /dev/null +++ b/app/javascript/mastodon/features/notifications/components/show_announcements_button.tsx @@ -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 ( + + ); +}; diff --git a/app/javascript/mastodon/features/notifications_v2/index.tsx b/app/javascript/mastodon/features/notifications_v2/index.tsx index 7e114c4a679..d524c408bbf 100644 --- a/app/javascript/mastodon/features/notifications_v2/index.tsx +++ b/app/javascript/mastodon/features/notifications_v2/index.tsx @@ -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 && } + {showAnnouncements && } ); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 458aa1058c0..13213d80c59 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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",