1
0
mirror of https://github.com/mastodon/mastodon synced 2024-11-30 15:58:14 +09:00

Add ability to group follow notifications in WebUI (#32520)

This commit is contained in:
Renaud Chaput 2024-10-16 10:33:11 +02:00 committed by GitHub
parent acc1973f3a
commit 6c87c76e18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 48 additions and 17 deletions

View File

@ -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),
};
}, },
); );

View File

@ -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>

View File

@ -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());
}
} }
}, },

View File

@ -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}

View File

@ -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",

View File

@ -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);

View File

@ -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({

View File

@ -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 */