From 4101f567c5da9f388f48cfd59ea805646dd6159a Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 28 May 2026 22:13:56 +0200 Subject: [PATCH] Collection notification filtering (#39198) --- .../mastodon/actions/notification_groups.ts | 15 +- .../mastodon/actions/notifications.js | 3 + .../components/column_settings.jsx | 6 + .../components/column_settings_group.tsx | 162 ++++++++++++++++++ .../features/notifications_v2/filter_bar.tsx | 13 ++ app/javascript/mastodon/locales/en.json | 2 + app/javascript/mastodon/reducers/settings.js | 3 + .../mastodon/selectors/notifications.ts | 5 +- app/javascript/mastodon/selectors/settings.ts | 17 +- 9 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 app/javascript/mastodon/features/notifications/components/column_settings_group.tsx diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 2d03ef080f..eddd6a9300 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -36,9 +36,18 @@ function notificationTypeForFilter(type: NotificationType) { } function notificationTypeForQuickFilter(type: NotificationType) { - if (type === 'quoted_update') return 'update'; - else if (type === 'quote') return 'mention'; - else return type; + switch (type) { + case 'quoted_update': + return 'update'; + case 'quote': + return 'mention'; + case 'collection_update': + return 'collection'; + case 'added_to_collection': + return 'collection'; + default: + return type; + } } function excludeAllTypesExcept(filter: string) { diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index da0c5f1102..cb4cd1251c 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -86,6 +86,9 @@ export function setupBrowserNotifications() { }; } +/** + * @param {(NotificationPermission) => void} callback + */ export function requestBrowserPermission(callback = noOp) { return dispatch => { requestNotificationPermission((permission) => { diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index b1f4e59818..588af106b5 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -12,6 +12,7 @@ import ClearColumnButton from './clear_column_button'; import GrantPermissionButton from './grant_permission_button'; import { PolicyControls } from './policy_controls'; import SettingToggle from './setting_toggle'; +import { ColumnSettingsGroup } from './column_settings_group'; class ColumnSettings extends PureComponent { static propTypes = { @@ -187,6 +188,11 @@ class ColumnSettings extends PureComponent { + } + /> + {((this.props.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (

diff --git a/app/javascript/mastodon/features/notifications/components/column_settings_group.tsx b/app/javascript/mastodon/features/notifications/components/column_settings_group.tsx new file mode 100644 index 0000000000..f888488be3 --- /dev/null +++ b/app/javascript/mastodon/features/notifications/components/column_settings_group.tsx @@ -0,0 +1,162 @@ +import { useCallback } from 'react'; +import type { FC, ReactNode } from 'react'; + +import { defineMessages, FormattedMessage } from 'react-intl'; + +import { showAlert } from '@/mastodon/actions/alerts'; +import { requestBrowserPermission } from '@/mastodon/actions/notifications'; +import { changeAlerts } from '@/mastodon/actions/push_notifications'; +import { changeSetting } from '@/mastodon/actions/settings'; +import { + createAppSelector, + useAppDispatch, + useAppSelector, +} from '@/mastodon/store'; + +import SettingToggle from './setting_toggle'; + +const selectNotificationSettings = createAppSelector( + [ + (state) => + (state.settings as Immutable.Map).get( + 'notifications', + ) as Immutable.Map | boolean>, + (state) => state.notifications.get('browserPermission') as string, + (state) => state.push_notifications, + ], + (settings, browserPermission, pushSettings) => ({ + settings, + browserPermission: browserPermission !== 'denied', + pushSettings, + showPushSettings: + pushSettings.get('supported') && pushSettings.get('enabled'), + }), +); + +const messages = defineMessages({ + permissionDenied: { + id: 'notifications.permission_denied_alert', + defaultMessage: + "Desktop notifications can't be enabled, as browser permission has been denied before", + }, +}); + +type SettingsType = 'alerts' | 'shows' | 'sounds'; + +export const ColumnSettingsGroup: FC<{ label: ReactNode; type: string }> = ({ + label, + type, +}) => { + const { settings, browserPermission, pushSettings, showPushSettings } = + useAppSelector(selectNotificationSettings); + + const dispatch = useAppDispatch(); + const handleChange = useCallback( + (path: [SettingsType, string], checked: boolean) => { + if ( + path[0] === 'alerts' && + checked && + typeof window.Notification !== 'undefined' && + Notification.permission !== 'granted' + ) { + dispatch( + requestBrowserPermission((permission) => { + if (permission === 'granted') { + dispatch(changeSetting(['notifications', ...path], checked)); + } else { + dispatch(showAlert({ message: messages.permissionDenied })); + } + }), + ); + } else { + dispatch(changeSetting(['notifications', ...path], checked)); + } + }, + [dispatch], + ); + const handlePushChange = useCallback( + (path: string[], checked: boolean) => { + if ( + checked && + typeof window.Notification !== 'undefined' && + Notification.permission !== 'granted' + ) { + dispatch( + requestBrowserPermission((permission: NotificationPermission) => { + if (permission === 'granted') { + dispatch(changeAlerts(path, checked)); + } else { + dispatch(showAlert({ message: messages.permissionDenied })); + } + }), + ); + } else { + dispatch(changeAlerts(path, checked)); + } + }, + [dispatch], + ); + + return ( +
+

{label}

+ +
+ + } + /> + + {showPushSettings && ( + + } + /> + )} + + + } + /> + + + } + /> +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/notifications_v2/filter_bar.tsx b/app/javascript/mastodon/features/notifications_v2/filter_bar.tsx index 56067afa93..f955276182 100644 --- a/app/javascript/mastodon/features/notifications_v2/filter_bar.tsx +++ b/app/javascript/mastodon/features/notifications_v2/filter_bar.tsx @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import CollectionsIcon from '@/material-icons/400-24px/category.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react'; @@ -26,6 +27,10 @@ const tooltips = defineMessages({ boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, + collections: { + id: 'notifications.filter.collections', + defaultMessage: 'Collections', + }, statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow', @@ -124,6 +129,14 @@ export const FilterBar: React.FC = () => { > + + + ); else diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index b97260173b..f111e436b8 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -1000,6 +1000,7 @@ "notifications.column_settings.admin.report": "New reports:", "notifications.column_settings.admin.sign_up": "New sign-ups:", "notifications.column_settings.alert": "Desktop notifications", + "notifications.column_settings.collections": "Collections:", "notifications.column_settings.favourite": "Favorites:", "notifications.column_settings.filter_bar.advanced": "Display all categories", "notifications.column_settings.filter_bar.category": "Quick filter bar", @@ -1019,6 +1020,7 @@ "notifications.column_settings.update": "Edits:", "notifications.filter.all": "All", "notifications.filter.boosts": "Boosts", + "notifications.filter.collections": "Collections", "notifications.filter.favourites": "Favorites", "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 775996a48c..c10d350600 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -41,6 +41,7 @@ const initialState = ImmutableMap({ poll: false, status: false, update: false, + collections: false, 'admin.sign_up': false, 'admin.report': false, }), @@ -65,6 +66,7 @@ const initialState = ImmutableMap({ poll: true, status: true, update: true, + collections: true, 'admin.sign_up': true, 'admin.report': true, }), @@ -79,6 +81,7 @@ const initialState = ImmutableMap({ poll: true, status: true, update: true, + collections: true, 'admin.sign_up': true, 'admin.report': true, }), diff --git a/app/javascript/mastodon/selectors/notifications.ts b/app/javascript/mastodon/selectors/notifications.ts index 8c808a2dff..14111176ac 100644 --- a/app/javascript/mastodon/selectors/notifications.ts +++ b/app/javascript/mastodon/selectors/notifications.ts @@ -29,7 +29,10 @@ const filterNotificationsByAllowedTypes = ( (item) => item.type === 'gap' || allowedType === item.type || - (allowedType === 'mention' && item.type === 'quote'), + (allowedType === 'mention' && item.type === 'quote') || + (allowedType === 'collection' && + (item.type === 'collection_update' || + item.type === 'added_to_collection')), ); }; diff --git a/app/javascript/mastodon/selectors/settings.ts b/app/javascript/mastodon/selectors/settings.ts index ca34374167..49e0df0c40 100644 --- a/app/javascript/mastodon/selectors/settings.ts +++ b/app/javascript/mastodon/selectors/settings.ts @@ -17,10 +17,19 @@ export const selectSettingsNotificationsShows = createSelector( export const selectSettingsNotificationsExcludedTypes = createSelector( [selectSettingsNotificationsShows], - (shows) => - Object.entries(shows) - .filter(([_type, enabled]) => !enabled) - .map(([type, _enabled]) => type), + (shows) => { + const excludedTypes: string[] = []; + for (const key in shows) { + if (!shows[key]) { + if (key === 'collections') { + excludedTypes.push('collection_update', 'added_to_collection'); + } else { + excludedTypes.push(key); + } + } + } + return excludedTypes; + }, ); export const selectSettingsNotificationsQuickFilterShow = (state: RootState) =>