Add ability to group follow notifications in WebUI (#32520)
parent
acc1973f3a
commit
6c87c76e18
|
@ -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…
Reference in New Issue