Add ability to group follow notifications in WebUI (#32520)
This commit is contained in:
		
					parent
					
						
							
								93348136a5
							
						
					
				
			
			
				commit
				
					
						e507b4f884
					
				
			
		
					 8 changed files with 48 additions and 17 deletions
				
			
		|  | @ -8,6 +8,7 @@ import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; | ||||||
| import type { | import type { | ||||||
|   ApiNotificationGroupJSON, |   ApiNotificationGroupJSON, | ||||||
|   ApiNotificationJSON, |   ApiNotificationJSON, | ||||||
|  |   NotificationType, | ||||||
| } from 'mastodon/api_types/notifications'; | } from 'mastodon/api_types/notifications'; | ||||||
| import { allNotificationTypes } from 'mastodon/api_types/notifications'; | import { allNotificationTypes } from 'mastodon/api_types/notifications'; | ||||||
| import type { ApiStatusJSON } from 'mastodon/api_types/statuses'; | 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 type { NotificationGap } from 'mastodon/reducers/notification_groups'; | ||||||
| import { | import { | ||||||
|   selectSettingsNotificationsExcludedTypes, |   selectSettingsNotificationsExcludedTypes, | ||||||
|  |   selectSettingsNotificationsGroupFollows, | ||||||
|   selectSettingsNotificationsQuickFilterActive, |   selectSettingsNotificationsQuickFilterActive, | ||||||
|   selectSettingsNotificationsShows, |   selectSettingsNotificationsShows, | ||||||
| } from 'mastodon/selectors/settings'; | } from 'mastodon/selectors/settings'; | ||||||
|  | @ -68,17 +70,19 @@ function dispatchAssociatedRecords( | ||||||
|     dispatch(importFetchedStatuses(fetchedStatuses)); |     dispatch(importFetchedStatuses(fetchedStatuses)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const supportedGroupedNotificationTypes = ['favourite', 'reblog']; | function selectNotificationGroupedTypes(state: RootState) { | ||||||
|  |   const types: NotificationType[] = ['favourite', 'reblog']; | ||||||
| 
 | 
 | ||||||
| export function shouldGroupNotificationType(type: string) { |   if (selectSettingsNotificationsGroupFollows(state)) types.push('follow'); | ||||||
|   return supportedGroupedNotificationTypes.includes(type); | 
 | ||||||
|  |   return types; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const fetchNotifications = createDataLoadingThunk( | export const fetchNotifications = createDataLoadingThunk( | ||||||
|   'notificationGroups/fetch', |   'notificationGroups/fetch', | ||||||
|   async (_params, { getState }) => |   async (_params, { getState }) => | ||||||
|     apiFetchNotificationGroups({ |     apiFetchNotificationGroups({ | ||||||
|       grouped_types: supportedGroupedNotificationTypes, |       grouped_types: selectNotificationGroupedTypes(getState()), | ||||||
|       exclude_types: getExcludedTypes(getState()), |       exclude_types: getExcludedTypes(getState()), | ||||||
|     }), |     }), | ||||||
|   ({ notifications, accounts, statuses }, { dispatch }) => { |   ({ notifications, accounts, statuses }, { dispatch }) => { | ||||||
|  | @ -102,7 +106,7 @@ export const fetchNotificationsGap = createDataLoadingThunk( | ||||||
|   'notificationGroups/fetchGap', |   'notificationGroups/fetchGap', | ||||||
|   async (params: { gap: NotificationGap }, { getState }) => |   async (params: { gap: NotificationGap }, { getState }) => | ||||||
|     apiFetchNotificationGroups({ |     apiFetchNotificationGroups({ | ||||||
|       grouped_types: supportedGroupedNotificationTypes, |       grouped_types: selectNotificationGroupedTypes(getState()), | ||||||
|       max_id: params.gap.maxId, |       max_id: params.gap.maxId, | ||||||
|       exclude_types: getExcludedTypes(getState()), |       exclude_types: getExcludedTypes(getState()), | ||||||
|     }), |     }), | ||||||
|  | @ -119,7 +123,7 @@ export const pollRecentNotifications = createDataLoadingThunk( | ||||||
|   'notificationGroups/pollRecentNotifications', |   'notificationGroups/pollRecentNotifications', | ||||||
|   async (_params, { getState }) => { |   async (_params, { getState }) => { | ||||||
|     return apiFetchNotificationGroups({ |     return apiFetchNotificationGroups({ | ||||||
|       grouped_types: supportedGroupedNotificationTypes, |       grouped_types: selectNotificationGroupedTypes(getState()), | ||||||
|       max_id: undefined, |       max_id: undefined, | ||||||
|       exclude_types: getExcludedTypes(getState()), |       exclude_types: getExcludedTypes(getState()), | ||||||
|       // In slow mode, we don't want to include notifications that duplicate the already-displayed ones
 |       // 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]); |     dispatchAssociatedRecords(dispatch, [notification]); | ||||||
| 
 | 
 | ||||||
|     return notification; |     return { | ||||||
|  |       notification, | ||||||
|  |       groupedTypes: selectNotificationGroupedTypes(state), | ||||||
|  |     }; | ||||||
|   }, |   }, | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -38,6 +38,7 @@ class ColumnSettings extends PureComponent { | ||||||
|     const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />; |     const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />; | ||||||
|     const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; |     const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; | ||||||
|     const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />; |     const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />; | ||||||
|  |     const groupStr = <FormattedMessage id='notifications.column_settings.group' defaultMessage='Group' />; | ||||||
| 
 | 
 | ||||||
|     const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); |     const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); | ||||||
|     const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />; |     const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />; | ||||||
|  | @ -94,6 +95,7 @@ class ColumnSettings extends PureComponent { | ||||||
|             {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} onChange={this.onPushChange} label={pushStr} />} |             {showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} onChange={this.onPushChange} label={pushStr} />} | ||||||
|             <SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} /> |             <SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} /> | ||||||
|             <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} /> |             <SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} /> | ||||||
|  |             <SettingToggle prefix='notifications' settings={settings} settingPath={['group', 'follow']} onChange={onChange} label={groupStr} /> | ||||||
|           </div> |           </div> | ||||||
|         </section> |         </section> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -56,11 +56,12 @@ const mapDispatchToProps = (dispatch) => ({ | ||||||
|       } else { |       } else { | ||||||
|         dispatch(changeSetting(['notifications', ...path], checked)); |         dispatch(changeSetting(['notifications', ...path], checked)); | ||||||
|       } |       } | ||||||
|     } else if(path[0] === 'groupingBeta') { |  | ||||||
|       dispatch(changeSetting(['notifications', ...path], checked)); |  | ||||||
|       dispatch(initializeNotifications()); |  | ||||||
|     } else { |     } else { | ||||||
|       dispatch(changeSetting(['notifications', ...path], checked)); |       dispatch(changeSetting(['notifications', ...path], checked)); | ||||||
|  | 
 | ||||||
|  |       if(path[0] === 'group' && path[1] === 'follow') { | ||||||
|  |         dispatch(initializeNotifications()); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,16 +1,19 @@ | ||||||
| import { FormattedMessage } from 'react-intl'; | import { FormattedMessage } from 'react-intl'; | ||||||
| 
 | 
 | ||||||
|  | import { Link } from 'react-router-dom'; | ||||||
|  | 
 | ||||||
| import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react'; | import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react'; | ||||||
| import { FollowersCounter } from 'mastodon/components/counters'; | import { FollowersCounter } from 'mastodon/components/counters'; | ||||||
| import { FollowButton } from 'mastodon/components/follow_button'; | import { FollowButton } from 'mastodon/components/follow_button'; | ||||||
| import { ShortNumber } from 'mastodon/components/short_number'; | import { ShortNumber } from 'mastodon/components/short_number'; | ||||||
|  | import { me } from 'mastodon/initial_state'; | ||||||
| import type { NotificationGroupFollow } from 'mastodon/models/notification_group'; | import type { NotificationGroupFollow } from 'mastodon/models/notification_group'; | ||||||
| import { useAppSelector } from 'mastodon/store'; | import { useAppSelector } from 'mastodon/store'; | ||||||
| 
 | 
 | ||||||
| import type { LabelRenderer } from './notification_group_with_status'; | import type { LabelRenderer } from './notification_group_with_status'; | ||||||
| import { NotificationGroupWithStatus } 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) |   if (total === 1) | ||||||
|     return ( |     return ( | ||||||
|       <FormattedMessage |       <FormattedMessage | ||||||
|  | @ -23,10 +26,12 @@ const labelRenderer: LabelRenderer = (displayedName, total) => { | ||||||
|   return ( |   return ( | ||||||
|     <FormattedMessage |     <FormattedMessage | ||||||
|       id='notification.follow.name_and_others' |       id='notification.follow.name_and_others' | ||||||
|       defaultMessage='{name} and {count, plural, one {# other} other {# others}} followed you' |       defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a> followed you' | ||||||
|       values={{ |       values={{ | ||||||
|         name: displayedName, |         name: displayedName, | ||||||
|         count: total - 1, |         count: total - 1, | ||||||
|  |         a: (chunks) => | ||||||
|  |           seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks, | ||||||
|       }} |       }} | ||||||
|     /> |     /> | ||||||
|   ); |   ); | ||||||
|  | @ -46,6 +51,10 @@ export const NotificationFollow: React.FC<{ | ||||||
|   notification: NotificationGroupFollow; |   notification: NotificationGroupFollow; | ||||||
|   unread: boolean; |   unread: boolean; | ||||||
| }> = ({ notification, unread }) => { | }> = ({ notification, unread }) => { | ||||||
|  |   const username = useAppSelector( | ||||||
|  |     (state) => state.accounts.getIn([me, 'username']) as string, | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|   let actions: JSX.Element | undefined; |   let actions: JSX.Element | undefined; | ||||||
|   let additionalContent: JSX.Element | undefined; |   let additionalContent: JSX.Element | undefined; | ||||||
| 
 | 
 | ||||||
|  | @ -68,6 +77,7 @@ export const NotificationFollow: React.FC<{ | ||||||
|       timestamp={notification.latest_page_notification_at} |       timestamp={notification.latest_page_notification_at} | ||||||
|       count={notification.notifications_count} |       count={notification.notifications_count} | ||||||
|       labelRenderer={labelRenderer} |       labelRenderer={labelRenderer} | ||||||
|  |       labelSeeMoreHref={`/@${username}/followers`} | ||||||
|       unread={unread} |       unread={unread} | ||||||
|       actions={actions} |       actions={actions} | ||||||
|       additionalContent={additionalContent} |       additionalContent={additionalContent} | ||||||
|  |  | ||||||
|  | @ -508,7 +508,7 @@ | ||||||
|   "notification.favourite": "{name} favorited your post", |   "notification.favourite": "{name} favorited your post", | ||||||
|   "notification.favourite.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your post", |   "notification.favourite.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favorited your post", | ||||||
|   "notification.follow": "{name} followed you", |   "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 <a>{count, plural, one {# other} other {# others}}</a> followed you", | ||||||
|   "notification.follow_request": "{name} has requested to follow 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.follow_request.name_and_others": "{name} and {count, plural, one {# other} other {# others}} has requested to follow you", | ||||||
|   "notification.label.mention": "Mention", |   "notification.label.mention": "Mention", | ||||||
|  | @ -567,6 +567,7 @@ | ||||||
|   "notifications.column_settings.filter_bar.category": "Quick filter bar", |   "notifications.column_settings.filter_bar.category": "Quick filter bar", | ||||||
|   "notifications.column_settings.follow": "New followers:", |   "notifications.column_settings.follow": "New followers:", | ||||||
|   "notifications.column_settings.follow_request": "New follow requests:", |   "notifications.column_settings.follow_request": "New follow requests:", | ||||||
|  |   "notifications.column_settings.group": "Group", | ||||||
|   "notifications.column_settings.mention": "Mentions:", |   "notifications.column_settings.mention": "Mentions:", | ||||||
|   "notifications.column_settings.poll": "Poll results:", |   "notifications.column_settings.poll": "Poll results:", | ||||||
|   "notifications.column_settings.push": "Push notifications", |   "notifications.column_settings.push": "Push notifications", | ||||||
|  |  | ||||||
|  | @ -21,7 +21,6 @@ import { | ||||||
|   unmountNotifications, |   unmountNotifications, | ||||||
|   refreshStaleNotificationGroups, |   refreshStaleNotificationGroups, | ||||||
|   pollRecentNotifications, |   pollRecentNotifications, | ||||||
|   shouldGroupNotificationType, |  | ||||||
| } from 'mastodon/actions/notification_groups'; | } from 'mastodon/actions/notification_groups'; | ||||||
| import { | import { | ||||||
|   disconnectTimeline, |   disconnectTimeline, | ||||||
|  | @ -30,6 +29,7 @@ import { | ||||||
| import type { | import type { | ||||||
|   ApiNotificationJSON, |   ApiNotificationJSON, | ||||||
|   ApiNotificationGroupJSON, |   ApiNotificationGroupJSON, | ||||||
|  |   NotificationType, | ||||||
| } from 'mastodon/api_types/notifications'; | } from 'mastodon/api_types/notifications'; | ||||||
| import { compareId } from 'mastodon/compare_id'; | import { compareId } from 'mastodon/compare_id'; | ||||||
| import { usePendingItems } from 'mastodon/initial_state'; | import { usePendingItems } from 'mastodon/initial_state'; | ||||||
|  | @ -205,8 +205,9 @@ function mergeGapsAround( | ||||||
| function processNewNotification( | function processNewNotification( | ||||||
|   groups: NotificationGroupsState['groups'], |   groups: NotificationGroupsState['groups'], | ||||||
|   notification: ApiNotificationJSON, |   notification: ApiNotificationJSON, | ||||||
|  |   groupedTypes: NotificationType[], | ||||||
| ) { | ) { | ||||||
|   if (!shouldGroupNotificationType(notification.type)) { |   if (!groupedTypes.includes(notification.type)) { | ||||||
|     notification = { |     notification = { | ||||||
|       ...notification, |       ...notification, | ||||||
|       group_key: `ungrouped-${notification.id}`, |       group_key: `ungrouped-${notification.id}`, | ||||||
|  | @ -476,11 +477,13 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>( | ||||||
|         trimNotifications(state); |         trimNotifications(state); | ||||||
|       }) |       }) | ||||||
|       .addCase(processNewNotificationForGroups.fulfilled, (state, action) => { |       .addCase(processNewNotificationForGroups.fulfilled, (state, action) => { | ||||||
|         const notification = action.payload; |         if (action.payload) { | ||||||
|         if (notification) { |           const { notification, groupedTypes } = action.payload; | ||||||
|  | 
 | ||||||
|           processNewNotification( |           processNewNotification( | ||||||
|             usePendingItems ? state.pendingGroups : state.groups, |             usePendingItems ? state.pendingGroups : state.groups, | ||||||
|             notification, |             notification, | ||||||
|  |             groupedTypes, | ||||||
|           ); |           ); | ||||||
|           updateLastReadId(state); |           updateLastReadId(state); | ||||||
|           trimNotifications(state); |           trimNotifications(state); | ||||||
|  |  | ||||||
|  | @ -78,6 +78,10 @@ const initialState = ImmutableMap({ | ||||||
|       'admin.sign_up': true, |       'admin.sign_up': true, | ||||||
|       'admin.report': true, |       'admin.report': true, | ||||||
|     }), |     }), | ||||||
|  | 
 | ||||||
|  |     group: ImmutableMap({ | ||||||
|  |       follow: true | ||||||
|  |     }), | ||||||
|   }), |   }), | ||||||
| 
 | 
 | ||||||
|   firehose: ImmutableMap({ |   firehose: ImmutableMap({ | ||||||
|  |  | ||||||
|  | @ -52,4 +52,7 @@ export const selectSettingsNotificationsMinimizeFilteredBanner = ( | ||||||
| ) => | ) => | ||||||
|   state.settings.getIn(['notifications', 'minimizeFilteredBanner']) as boolean; |   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 */ | /* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue