From 6c87c76e1825a377ebb87fdfcf7f5c438c1be589 Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Wed, 16 Oct 2024 10:33:11 +0200 Subject: [PATCH] Add ability to group follow notifications in WebUI (#32520) --- .../mastodon/actions/notification_groups.ts | 21 ++++++++++++------- .../components/column_settings.jsx | 2 ++ .../containers/column_settings_container.js | 7 ++++--- .../components/notification_follow.tsx | 14 +++++++++++-- app/javascript/mastodon/locales/en.json | 3 ++- .../mastodon/reducers/notification_groups.ts | 11 ++++++---- app/javascript/mastodon/reducers/settings.js | 4 ++++ app/javascript/mastodon/selectors/settings.ts | 3 +++ 8 files changed, 48 insertions(+), 17 deletions(-) diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index a359913e61..a3c8095ac4 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -8,6 +8,7 @@ import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; import type { ApiNotificationGroupJSON, ApiNotificationJSON, + NotificationType, } from 'mastodon/api_types/notifications'; import { allNotificationTypes } from 'mastodon/api_types/notifications'; import type { ApiStatusJSON } from 'mastodon/api_types/statuses'; @@ -15,6 +16,7 @@ import { usePendingItems } from 'mastodon/initial_state'; import type { NotificationGap } from 'mastodon/reducers/notification_groups'; import { selectSettingsNotificationsExcludedTypes, + selectSettingsNotificationsGroupFollows, selectSettingsNotificationsQuickFilterActive, selectSettingsNotificationsShows, } from 'mastodon/selectors/settings'; @@ -68,17 +70,19 @@ function dispatchAssociatedRecords( dispatch(importFetchedStatuses(fetchedStatuses)); } -const supportedGroupedNotificationTypes = ['favourite', 'reblog']; +function selectNotificationGroupedTypes(state: RootState) { + const types: NotificationType[] = ['favourite', 'reblog']; -export function shouldGroupNotificationType(type: string) { - return supportedGroupedNotificationTypes.includes(type); + if (selectSettingsNotificationsGroupFollows(state)) types.push('follow'); + + return types; } export const fetchNotifications = createDataLoadingThunk( 'notificationGroups/fetch', async (_params, { getState }) => apiFetchNotificationGroups({ - grouped_types: supportedGroupedNotificationTypes, + grouped_types: selectNotificationGroupedTypes(getState()), exclude_types: getExcludedTypes(getState()), }), ({ notifications, accounts, statuses }, { dispatch }) => { @@ -102,7 +106,7 @@ export const fetchNotificationsGap = createDataLoadingThunk( 'notificationGroups/fetchGap', async (params: { gap: NotificationGap }, { getState }) => apiFetchNotificationGroups({ - grouped_types: supportedGroupedNotificationTypes, + grouped_types: selectNotificationGroupedTypes(getState()), max_id: params.gap.maxId, exclude_types: getExcludedTypes(getState()), }), @@ -119,7 +123,7 @@ export const pollRecentNotifications = createDataLoadingThunk( 'notificationGroups/pollRecentNotifications', async (_params, { getState }) => { return apiFetchNotificationGroups({ - grouped_types: supportedGroupedNotificationTypes, + grouped_types: selectNotificationGroupedTypes(getState()), max_id: undefined, exclude_types: getExcludedTypes(getState()), // In slow mode, we don't want to include notifications that duplicate the already-displayed ones @@ -168,7 +172,10 @@ export const processNewNotificationForGroups = createAppAsyncThunk( dispatchAssociatedRecords(dispatch, [notification]); - return notification; + return { + notification, + groupedTypes: selectNotificationGroupedTypes(state), + }; }, ); diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index ed2947c175..9616adcb93 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -38,6 +38,7 @@ class ColumnSettings extends PureComponent { const alertStr = ; const showStr = ; const soundStr = ; + const groupStr = ; const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); const pushStr = showPushSettings && ; @@ -94,6 +95,7 @@ class ColumnSettings extends PureComponent { {showPushSettings && } + diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index 8bcc7ab4ef..4ac6cfa629 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -56,11 +56,12 @@ const mapDispatchToProps = (dispatch) => ({ } else { dispatch(changeSetting(['notifications', ...path], checked)); } - } else if(path[0] === 'groupingBeta') { - dispatch(changeSetting(['notifications', ...path], checked)); - dispatch(initializeNotifications()); } else { dispatch(changeSetting(['notifications', ...path], checked)); + + if(path[0] === 'group' && path[1] === 'follow') { + dispatch(initializeNotifications()); + } } }, diff --git a/app/javascript/mastodon/features/notifications_v2/components/notification_follow.tsx b/app/javascript/mastodon/features/notifications_v2/components/notification_follow.tsx index 6a9a45d242..2c90777b9f 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/notification_follow.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/notification_follow.tsx @@ -1,16 +1,19 @@ import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router-dom'; + import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react'; import { FollowersCounter } from 'mastodon/components/counters'; import { FollowButton } from 'mastodon/components/follow_button'; import { ShortNumber } from 'mastodon/components/short_number'; +import { me } from 'mastodon/initial_state'; import type { NotificationGroupFollow } from 'mastodon/models/notification_group'; import { useAppSelector } from 'mastodon/store'; import type { LabelRenderer } from './notification_group_with_status'; import { NotificationGroupWithStatus } from './notification_group_with_status'; -const labelRenderer: LabelRenderer = (displayedName, total) => { +const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => { if (total === 1) return ( { return ( + seeMoreHref ? {chunks} : chunks, }} /> ); @@ -46,6 +51,10 @@ export const NotificationFollow: React.FC<{ notification: NotificationGroupFollow; unread: boolean; }> = ({ notification, unread }) => { + const username = useAppSelector( + (state) => state.accounts.getIn([me, 'username']) as string, + ); + let actions: JSX.Element | undefined; let additionalContent: JSX.Element | undefined; @@ -68,6 +77,7 @@ export const NotificationFollow: React.FC<{ timestamp={notification.latest_page_notification_at} count={notification.notifications_count} labelRenderer={labelRenderer} + labelSeeMoreHref={`/@${username}/followers`} unread={unread} actions={actions} additionalContent={additionalContent} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 7f8dc74779..eb29c03712 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -508,7 +508,7 @@ "notification.favourite": "{name} favorited your post", "notification.favourite.name_and_others_with_link": "{name} and {count, plural, one {# other} other {# others}} favorited your post", "notification.follow": "{name} followed you", - "notification.follow.name_and_others": "{name} and {count, plural, one {# other} other {# others}} followed you", + "notification.follow.name_and_others": "{name} and {count, plural, one {# other} other {# others}} followed you", "notification.follow_request": "{name} has requested to follow you", "notification.follow_request.name_and_others": "{name} and {count, plural, one {# other} other {# others}} has requested to follow you", "notification.label.mention": "Mention", @@ -567,6 +567,7 @@ "notifications.column_settings.filter_bar.category": "Quick filter bar", "notifications.column_settings.follow": "New followers:", "notifications.column_settings.follow_request": "New follow requests:", + "notifications.column_settings.group": "Group", "notifications.column_settings.mention": "Mentions:", "notifications.column_settings.poll": "Poll results:", "notifications.column_settings.push": "Push notifications", diff --git a/app/javascript/mastodon/reducers/notification_groups.ts b/app/javascript/mastodon/reducers/notification_groups.ts index 91e91d7549..7a165f5fec 100644 --- a/app/javascript/mastodon/reducers/notification_groups.ts +++ b/app/javascript/mastodon/reducers/notification_groups.ts @@ -21,7 +21,6 @@ import { unmountNotifications, refreshStaleNotificationGroups, pollRecentNotifications, - shouldGroupNotificationType, } from 'mastodon/actions/notification_groups'; import { disconnectTimeline, @@ -30,6 +29,7 @@ import { import type { ApiNotificationJSON, ApiNotificationGroupJSON, + NotificationType, } from 'mastodon/api_types/notifications'; import { compareId } from 'mastodon/compare_id'; import { usePendingItems } from 'mastodon/initial_state'; @@ -205,8 +205,9 @@ function mergeGapsAround( function processNewNotification( groups: NotificationGroupsState['groups'], notification: ApiNotificationJSON, + groupedTypes: NotificationType[], ) { - if (!shouldGroupNotificationType(notification.type)) { + if (!groupedTypes.includes(notification.type)) { notification = { ...notification, group_key: `ungrouped-${notification.id}`, @@ -476,11 +477,13 @@ export const notificationGroupsReducer = createReducer( trimNotifications(state); }) .addCase(processNewNotificationForGroups.fulfilled, (state, action) => { - const notification = action.payload; - if (notification) { + if (action.payload) { + const { notification, groupedTypes } = action.payload; + processNewNotification( usePendingItems ? state.pendingGroups : state.groups, notification, + groupedTypes, ); updateLastReadId(state); trimNotifications(state); diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index e5ff2ff910..fc02ac7186 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -78,6 +78,10 @@ const initialState = ImmutableMap({ 'admin.sign_up': true, 'admin.report': true, }), + + group: ImmutableMap({ + follow: true + }), }), firehose: ImmutableMap({ diff --git a/app/javascript/mastodon/selectors/settings.ts b/app/javascript/mastodon/selectors/settings.ts index e722ad0911..ca34374167 100644 --- a/app/javascript/mastodon/selectors/settings.ts +++ b/app/javascript/mastodon/selectors/settings.ts @@ -52,4 +52,7 @@ export const selectSettingsNotificationsMinimizeFilteredBanner = ( ) => state.settings.getIn(['notifications', 'minimizeFilteredBanner']) as boolean; +export const selectSettingsNotificationsGroupFollows = (state: RootState) => + state.settings.getIn(['notifications', 'group', 'follow']) as boolean; + /* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */