Collection notification filtering (#39198)

This commit is contained in:
Echo 2026-05-28 22:13:56 +02:00 committed by GitHub
parent faa5944d46
commit 4101f567c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 218 additions and 8 deletions

View File

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

View File

@ -86,6 +86,9 @@ export function setupBrowserNotifications() {
};
}
/**
* @param {(NotificationPermission) => void} callback
*/
export function requestBrowserPermission(callback = noOp) {
return dispatch => {
requestNotificationPermission((permission) => {

View File

@ -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 {
</div>
</section>
<ColumnSettingsGroup
type="collections"
label={<FormattedMessage id='notifications.column_settings.collections' defaultMessage='Collections:' />}
/>
{((this.props.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (
<section role='group' aria-labelledby='notifications-admin-sign-up'>
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></h3>

View File

@ -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<string, unknown>).get(
'notifications',
) as Immutable.Map<string, Immutable.Map<string, unknown> | 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 (
<section role='group' aria-labelledby={`notifications-${type}`}>
<h3 id={`notifications-${type}`}>{label}</h3>
<div className='column-settings__row'>
<SettingToggle
disabled={!browserPermission}
prefix='notifications_desktop'
settings={settings}
settingPath={['alerts', type]}
onChange={handleChange}
label={
<FormattedMessage
id='notifications.column_settings.alert'
defaultMessage='Desktop notifications'
/>
}
/>
{showPushSettings && (
<SettingToggle
prefix='notifications_push'
settings={pushSettings}
settingPath={['alerts', type]}
onChange={handlePushChange}
label={
<FormattedMessage
id='notifications.column_settings.push'
defaultMessage='Push notifications'
/>
}
/>
)}
<SettingToggle
prefix='notifications'
settings={settings}
settingPath={['shows', type]}
onChange={handleChange}
label={
<FormattedMessage
id='notifications.column_settings.show'
defaultMessage='Show in column'
/>
}
/>
<SettingToggle
prefix='notifications'
settings={settings}
settingPath={['sounds', type]}
onChange={handleChange}
label={
<FormattedMessage
id='notifications.column_settings.sound'
defaultMessage='Play sound'
/>
}
/>
</div>
</section>
);
};

View File

@ -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 = () => {
>
<Icon id='user-plus' icon={PersonAddIcon} />
</BarButton>
<BarButton
selectedFilter={selectedFilter}
type='collection'
key='collection'
title={intl.formatMessage(tooltips.collections)}
>
<Icon id='collections' icon={CollectionsIcon} />
</BarButton>
</div>
);
else

View File

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

View File

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

View File

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

View File

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