diff --git a/app/javascript/mastodon/actions/announcements.js b/app/javascript/mastodon/actions/announcements.js index 7657b05dc42..4f2616e835d 100644 --- a/app/javascript/mastodon/actions/announcements.js +++ b/app/javascript/mastodon/actions/announcements.js @@ -23,6 +23,8 @@ export const ANNOUNCEMENTS_REACTION_REMOVE_FAIL = 'ANNOUNCEMENTS_REACTION_REM export const ANNOUNCEMENTS_REACTION_UPDATE = 'ANNOUNCEMENTS_REACTION_UPDATE'; export const ANNOUNCEMENTS_TOGGLE_SHOW = 'ANNOUNCEMENTS_TOGGLE_SHOW'; +export const ANNOUNCEMENTS_SHOW = 'ANNOUNCEMENTS_TOGGLE_SHOW'; +export const ANNOUNCEMENTS_HIDE = 'ANNOUNCEMENTS_TOGGLE_SHOW'; const noOp = () => {}; @@ -175,6 +177,14 @@ export const toggleShowAnnouncements = () => ({ type: ANNOUNCEMENTS_TOGGLE_SHOW, }); +export const hideAnnouncements = () => ({ + type: ANNOUNCEMENTS_HIDE, +}); + +export const showAnnouncements = () => ({ + type: ANNOUNCEMENTS_SHOW, +}); + export const deleteAnnouncement = id => ({ type: ANNOUNCEMENTS_DELETE, id, diff --git a/app/javascript/mastodon/features/announcements/announcement.tsx b/app/javascript/mastodon/features/announcements/announcement.tsx index 75be0ffe1b4..246dec6c750 100644 --- a/app/javascript/mastodon/features/announcements/announcement.tsx +++ b/app/javascript/mastodon/features/announcements/announcement.tsx @@ -57,6 +57,7 @@ export const Announcement: FC = ({ {' · '} + {!isVisuallyRead && } @@ -67,8 +68,6 @@ export const Announcement: FC = ({ /> - - {!isVisuallyRead && } ); }; diff --git a/app/javascript/mastodon/features/announcements/hooks.ts b/app/javascript/mastodon/features/announcements/hooks.ts index ec9884bb611..88ba7b7b6d4 100644 --- a/app/javascript/mastodon/features/announcements/hooks.ts +++ b/app/javascript/mastodon/features/announcements/hooks.ts @@ -1,19 +1,19 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import type { List as ImmutableList, Record as ImmutableRecord, } from 'immutable'; -import { fetchAnnouncements } from '@/mastodon/actions/announcements'; +import { + fetchAnnouncements, + showAnnouncements, +} from '@/mastodon/actions/announcements'; +import type { ApiAnnouncementJSON } from '@/mastodon/api_types/announcements'; import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import { isRedesignEnabled } from '@/mastodon/utils/environment'; -type AnnouncementItem = ImmutableRecord<{ - id: string; - starts_at: string; - published_at: string; - read: boolean; -}>; +type AnnouncementItem = ImmutableRecord; type AnnouncementsState = ImmutableRecord<{ items: ImmutableList; @@ -39,17 +39,11 @@ export function useHasAnnouncements({ ); 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(() => { + if (hasUnreadAnnouncements && isRedesignEnabled()) { + dispatch(showAnnouncements()); + } + }, [hasUnreadAnnouncements, dispatch]); useEffect(() => { const timeout = setTimeout(() => { @@ -66,7 +60,7 @@ export function useHasAnnouncements({ return { hasAnnouncements, hasUnreadAnnouncements, - showAnnouncements, + shouldShowAnnouncements, unreadAnnouncementCount, }; } diff --git a/app/javascript/mastodon/features/announcements/index.tsx b/app/javascript/mastodon/features/announcements/index.tsx index 2e4cc0214c4..efe85bc3d11 100644 --- a/app/javascript/mastodon/features/announcements/index.tsx +++ b/app/javascript/mastodon/features/announcements/index.tsx @@ -1,15 +1,26 @@ import { useCallback } from 'react'; import type { FC } from 'react'; +import { FormattedMessage } from 'react-intl'; + import type { Map, List } from 'immutable'; +import { XIcon } from '@phosphor-icons/react'; + import elephantUIPlane from '@/images/elephant_ui_plane.svg'; +import { hideAnnouncements } from '@/mastodon/actions/announcements'; +import { IconButton } from '@/mastodon/components/button/redesign'; import type { RenderSlideFn } from '@/mastodon/components/carousel'; import { Carousel } from '@/mastodon/components/carousel'; import { CustomEmojiProvider } from '@/mastodon/components/emoji/context'; import { useCustomEmojis } from '@/mastodon/hooks/useCustomEmojis'; import { mascot } from '@/mastodon/initial_state'; -import { createAppSelector, useAppSelector } from '@/mastodon/store'; +import { + createAppSelector, + useAppDispatch, + useAppSelector, +} from '@/mastodon/store'; +import { isRedesignEnabled } from '@/mastodon/utils/environment'; import type { IAnnouncement } from './announcement'; import { Announcement } from './announcement'; @@ -23,9 +34,14 @@ const announcementSelector = createAppSelector( ); export const Announcements: FC = () => { + const dispatch = useAppDispatch(); const announcements = useAppSelector(announcementSelector); const emojis = useCustomEmojis(); + const closeAnnouncements = useCallback(() => { + dispatch(hideAnnouncements()); + }, [dispatch]); + const renderSlide: RenderSlideFn<{ id: string; announcement: IAnnouncement; @@ -53,6 +69,18 @@ export const Announcements: FC = () => { src={mascot ?? elephantUIPlane} /> + {isRedesignEnabled() && ( + + + + )} + { const dispatch = useAppDispatch(); - const { hasAnnouncements, showAnnouncements } = useHasAnnouncements({ + const { hasAnnouncements, shouldShowAnnouncements } = useHasAnnouncements({ fetch: false, }); const handleClick = useCallback(() => { - dispatch(toggleShowAnnouncements()); + dispatch(showAnnouncements()); dispatch( closeModal({ modalType: 'NOTIFICATION_SETTINGS', ignoreFocus: false }), ); }, [dispatch]); - if (!isRedesignEnabled() || !hasAnnouncements || showAnnouncements) { + if (!isRedesignEnabled() || !hasAnnouncements || shouldShowAnnouncements) { return null; } diff --git a/app/javascript/mastodon/features/notifications_v2/index.tsx b/app/javascript/mastodon/features/notifications_v2/index.tsx index d524c408bbf..024fd4b44fb 100644 --- a/app/javascript/mastodon/features/notifications_v2/index.tsx +++ b/app/javascript/mastodon/features/notifications_v2/index.tsx @@ -222,12 +222,12 @@ export const Notifications: React.FC<{ ); }, [notifications, isLoading, hasMore, lastReadId, handleLoadGap]); - const { showAnnouncements } = useHasAnnouncements(); + const { shouldShowAnnouncements } = useHasAnnouncements(); const prepend = ( <> {needsNotificationPermission && } - {showAnnouncements && } + {shouldShowAnnouncements && } ); diff --git a/app/javascript/mastodon/reducers/announcements.js b/app/javascript/mastodon/reducers/announcements.js index 2134b04c6df..a938c683717 100644 --- a/app/javascript/mastodon/reducers/announcements.js +++ b/app/javascript/mastodon/reducers/announcements.js @@ -13,6 +13,8 @@ import { ANNOUNCEMENTS_TOGGLE_SHOW, ANNOUNCEMENTS_DELETE, ANNOUNCEMENTS_DISMISS_SUCCESS, + ANNOUNCEMENTS_SHOW, + ANNOUNCEMENTS_HIDE, } from '../actions/announcements'; const initialState = ImmutableMap({ @@ -64,6 +66,10 @@ export default function announcementsReducer(state = initialState, action) { return state.withMutations(map => { map.set('show', !map.get('show')); }); + case ANNOUNCEMENTS_SHOW: + return state.set('show', true); + case ANNOUNCEMENTS_HIDE: + return state.set('show', false); case ANNOUNCEMENTS_FETCH_REQUEST: return state.set('isLoading', true); case ANNOUNCEMENTS_FETCH_SUCCESS: diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 6312b33bfb4..0fe1a1cb2c0 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -8543,6 +8543,13 @@ noscript { width: 100%; } + &__close-button { + position: absolute; + top: var(--space-2); + inset-inline-end: var(--space-2); + z-index: 1; + } + &__root { background: var(--color-bg-brand-softest); font-size: 13px; @@ -8584,18 +8591,17 @@ noscript { display: block; font-weight: 500; margin-bottom: 10px; - padding-inline-end: 18px; + padding-inline-end: 28px; } &__unread { - position: absolute; - top: 19px; - inset-inline-end: 19px; - display: block; - background: var(--color-text-brand); - border-radius: 50%; + display: inline-block; width: 0.625rem; height: 0.625rem; + margin-inline-start: 8px; + border-radius: 50%; + vertical-align: 0.05em; + background: var(--color-text-brand); } &__pagination {