From 6511e595ecf36205a1745965de4563aaea67f9b6 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 25 Sep 2026 12:00:03 +0000 Subject: [PATCH] Redesign: Add follow requests indicator to notifications page (#40683) --- .../navigation_panel/redesign/index.tsx | 31 ++++++++++++- .../components/follow_requests_banner.tsx | 46 +++++++++++++++++++ .../features/notifications_v2/index.tsx | 2 + app/javascript/mastodon/locales/en.json | 1 + .../mastodon/selectors/notifications.ts | 30 +++++++----- 5 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 app/javascript/mastodon/features/notifications_v2/components/follow_requests_banner.tsx diff --git a/app/javascript/mastodon/features/navigation_panel/redesign/index.tsx b/app/javascript/mastodon/features/navigation_panel/redesign/index.tsx index 6f872a08621..7928a2f59ed 100644 --- a/app/javascript/mastodon/features/navigation_panel/redesign/index.tsx +++ b/app/javascript/mastodon/features/navigation_panel/redesign/index.tsx @@ -2,6 +2,8 @@ import { useCallback, useEffect } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import type { Map as ImmutableMap } from 'immutable'; + import { PenNibIcon, HouseIcon, @@ -14,6 +16,7 @@ import { } from '@phosphor-icons/react'; import FediIcon from '@/images/icons/icon_fediverse.svg?react'; +import { fetchFollowRequests } from '@/mastodon/actions/accounts'; import { fetchLists } from '@/mastodon/actions/lists'; import { closeNavigation } from '@/mastodon/actions/navigation'; import { fetchFollowedHashtags } from '@/mastodon/actions/tags_typed'; @@ -78,13 +81,39 @@ function useFollowedHashtags() { return { followedHashtags: tags }; } +export function useFollowRequestsCount({ + fetch = true, +}: { fetch?: boolean } = {}) { + const followRequestsCount = useAppSelector( + (state) => + ( + state.user_lists.getIn(['follow_requests', 'items']) as + | ImmutableMap + | undefined + )?.size ?? 0, + ); + const dispatch = useAppDispatch(); + + useEffect(() => { + if (fetch) { + dispatch(fetchFollowRequests()); + } + }, [dispatch, fetch]); + + return followRequestsCount; +} + export function useNotificationsCount() { const unreadNotificationsCount = useAppSelector( selectUnreadNotificationGroupsCount, ); + const followRequestsCount = useFollowRequestsCount(); + const { unreadAnnouncementCount } = useHasAnnouncements(); - return unreadNotificationsCount + unreadAnnouncementCount; + return ( + unreadNotificationsCount + followRequestsCount + unreadAnnouncementCount + ); } const isFediverseFeedsLinkActive = ( diff --git a/app/javascript/mastodon/features/notifications_v2/components/follow_requests_banner.tsx b/app/javascript/mastodon/features/notifications_v2/components/follow_requests_banner.tsx new file mode 100644 index 00000000000..c20fa7587b2 --- /dev/null +++ b/app/javascript/mastodon/features/notifications_v2/components/follow_requests_banner.tsx @@ -0,0 +1,46 @@ +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import { UserPlusIcon } from '@phosphor-icons/react'; + +import { Icon } from 'mastodon/components/icon'; + +import { useFollowRequestsCount } from '../../navigation_panel/redesign'; + +/** + * This banner is only shown to users whose notification policy filters + * or blocks notifications from accounts that are not following them, + * so that they have a way of finding out about pending requests. + */ +export const FollowRequestsBanner: React.FC = () => { + const followRequestsCount = useFollowRequestsCount({ fetch: false }); + + if (followRequestsCount === 0) { + return null; + } + + return ( + +
+ +
+ +
+ + + + + + +
+ + ); +}; diff --git a/app/javascript/mastodon/features/notifications_v2/index.tsx b/app/javascript/mastodon/features/notifications_v2/index.tsx index 024fd4b44fb..6ec3ccb45c5 100644 --- a/app/javascript/mastodon/features/notifications_v2/index.tsx +++ b/app/javascript/mastodon/features/notifications_v2/index.tsx @@ -61,6 +61,7 @@ import { import NotificationsPermissionBanner from '../notifications/components/notifications_permission_banner'; import ColumnSettingsContainer from '../notifications/containers/column_settings_container'; +import { FollowRequestsBanner } from './components/follow_requests_banner'; import { NotificationGroup } from './components/notification_group'; import { FilterBar } from './filter_bar'; @@ -229,6 +230,7 @@ export const Notifications: React.FC<{ {needsNotificationPermission && } {shouldShowAnnouncements && } + ); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 34b6bd94948..af5835a4e8b 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -840,6 +840,7 @@ "firehose.remote": "Other servers", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", + "follow_requests.pending_requests": "From {count, plural, =0 {no one} one {one person} other {# people}} you may know", "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", "follow_suggestions.curated_suggestion": "Staff pick", "follow_suggestions.dismiss": "Don't show again", diff --git a/app/javascript/mastodon/selectors/notifications.ts b/app/javascript/mastodon/selectors/notifications.ts index 14111176ac6..2edeec1500f 100644 --- a/app/javascript/mastodon/selectors/notifications.ts +++ b/app/javascript/mastodon/selectors/notifications.ts @@ -5,6 +5,8 @@ import type { NotificationGroup } from 'mastodon/models/notification_group'; import type { NotificationGap } from 'mastodon/reducers/notification_groups'; import type { RootState } from 'mastodon/store'; +import { isRedesignEnabled } from '../utils/environment'; + import { selectSettingsNotificationsExcludedTypes, selectSettingsNotificationsQuickFilterActive, @@ -63,19 +65,23 @@ export const selectUnreadNotificationGroupsCount = createSelector( selectPendingNotificationGroups, ], (notificationMarker, groups, pendingGroups) => { + function filterRelevantNotifications( + group: NotificationGap | NotificationGroup, + ) { + if (isRedesignEnabled() && group.type === 'follow_request') { + // Pending follow requests are counted separately in the redesign + return false; + } + return ( + group.type !== 'gap' && + group.page_max_id && + compareId(group.page_max_id, notificationMarker) > 0 + ); + } + return ( - groups.filter( - (group) => - group.type !== 'gap' && - group.page_max_id && - compareId(group.page_max_id, notificationMarker) > 0, - ).length + - pendingGroups.filter( - (group) => - group.type !== 'gap' && - group.page_max_id && - compareId(group.page_max_id, notificationMarker) > 0, - ).length + groups.filter(filterRelevantNotifications).length + + pendingGroups.filter(filterRelevantNotifications).length ); }, );