Add ability to group follow notifications in WebUI (#32520)
This commit is contained in:
		@@ -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),
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -38,6 +38,7 @@ class ColumnSettings extends PureComponent {
 | 
			
		||||
    const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
 | 
			
		||||
    const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
 | 
			
		||||
    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 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} />}
 | 
			
		||||
            <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={['group', 'follow']} onChange={onChange} label={groupStr} />
 | 
			
		||||
          </div>
 | 
			
		||||
        </section>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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());
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 (
 | 
			
		||||
      <FormattedMessage
 | 
			
		||||
@@ -23,10 +26,12 @@ const labelRenderer: LabelRenderer = (displayedName, total) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <FormattedMessage
 | 
			
		||||
      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={{
 | 
			
		||||
        name: displayedName,
 | 
			
		||||
        count: total - 1,
 | 
			
		||||
        a: (chunks) =>
 | 
			
		||||
          seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : 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}
 | 
			
		||||
 
 | 
			
		||||
@@ -508,7 +508,7 @@
 | 
			
		||||
  "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.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_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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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<NotificationGroupsState>(
 | 
			
		||||
        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);
 | 
			
		||||
 
 | 
			
		||||
@@ -78,6 +78,10 @@ const initialState = ImmutableMap({
 | 
			
		||||
      'admin.sign_up': true,
 | 
			
		||||
      'admin.report': true,
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    group: ImmutableMap({
 | 
			
		||||
      follow: true
 | 
			
		||||
    }),
 | 
			
		||||
  }),
 | 
			
		||||
 | 
			
		||||
  firehose: ImmutableMap({
 | 
			
		||||
 
 | 
			
		||||
@@ -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 */
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user