diff --git a/app/javascript/mastodon/actions/notification_policies.ts b/app/javascript/mastodon/actions/notification_policies.ts new file mode 100644 index 0000000000..fcc9919c49 --- /dev/null +++ b/app/javascript/mastodon/actions/notification_policies.ts @@ -0,0 +1,16 @@ +import { + apiGetNotificationPolicy, + apiUpdateNotificationsPolicy, +} from 'mastodon/api/notification_policies'; +import type { NotificationPolicy } from 'mastodon/models/notification_policy'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +export const fetchNotificationPolicy = createDataLoadingThunk( + 'notificationPolicy/fetch', + () => apiGetNotificationPolicy(), +); + +export const updateNotificationsPolicy = createDataLoadingThunk( + 'notificationPolicy/update', + (policy: Partial) => apiUpdateNotificationsPolicy(policy), +); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index fe728aa26e..6a59d5624e 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -44,10 +44,6 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; -export const NOTIFICATION_POLICY_FETCH_REQUEST = 'NOTIFICATION_POLICY_FETCH_REQUEST'; -export const NOTIFICATION_POLICY_FETCH_SUCCESS = 'NOTIFICATION_POLICY_FETCH_SUCCESS'; -export const NOTIFICATION_POLICY_FETCH_FAIL = 'NOTIFICATION_POLICY_FETCH_FAIL'; - export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST'; export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS'; export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL'; @@ -346,40 +342,6 @@ export function setBrowserPermission (value) { }; } -export const fetchNotificationPolicy = () => (dispatch) => { - dispatch(fetchNotificationPolicyRequest()); - - api().get('/api/v1/notifications/policy').then(({ data }) => { - dispatch(fetchNotificationPolicySuccess(data)); - }).catch(err => { - dispatch(fetchNotificationPolicyFail(err)); - }); -}; - -export const fetchNotificationPolicyRequest = () => ({ - type: NOTIFICATION_POLICY_FETCH_REQUEST, -}); - -export const fetchNotificationPolicySuccess = policy => ({ - type: NOTIFICATION_POLICY_FETCH_SUCCESS, - policy, -}); - -export const fetchNotificationPolicyFail = error => ({ - type: NOTIFICATION_POLICY_FETCH_FAIL, - error, -}); - -export const updateNotificationsPolicy = params => (dispatch) => { - dispatch(fetchNotificationPolicyRequest()); - - api().put('/api/v1/notifications/policy', params).then(({ data }) => { - dispatch(fetchNotificationPolicySuccess(data)); - }).catch(err => { - dispatch(fetchNotificationPolicyFail(err)); - }); -}; - export const fetchNotificationRequests = () => (dispatch, getState) => { const params = {}; diff --git a/app/javascript/mastodon/api/notification_policies.ts b/app/javascript/mastodon/api/notification_policies.ts new file mode 100644 index 0000000000..b2a1e5ac31 --- /dev/null +++ b/app/javascript/mastodon/api/notification_policies.ts @@ -0,0 +1,10 @@ +import { apiRequest } from 'mastodon/api'; +import type { NotificationPolicyJSON } from 'mastodon/api_types/notification_policies'; + +export const apiGetNotificationPolicy = () => + apiRequest('GET', '/v1/notifications/policy'); + +export const apiUpdateNotificationsPolicy = ( + policy: Partial, +) => + apiRequest('PUT', '/v1/notifications/policy', policy); diff --git a/app/javascript/mastodon/api_types/notification_policies.ts b/app/javascript/mastodon/api_types/notification_policies.ts new file mode 100644 index 0000000000..0f4a2d132e --- /dev/null +++ b/app/javascript/mastodon/api_types/notification_policies.ts @@ -0,0 +1,12 @@ +// See app/serializers/rest/notification_policy_serializer.rb + +export interface NotificationPolicyJSON { + filter_not_following: boolean; + filter_not_followers: boolean; + filter_new_accounts: boolean; + filter_private_mentions: boolean; + summary: { + pending_requests_count: number; + pending_notifications_count: number; + }; +} diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index e375b856c9..39e394e449 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -24,7 +24,7 @@ class ColumnSettings extends PureComponent { alertsEnabled: PropTypes.bool, browserSupport: PropTypes.bool, browserPermission: PropTypes.string, - notificationPolicy: ImmutablePropTypes.map, + notificationPolicy: PropTypes.object.isRequired, onChangePolicy: PropTypes.func.isRequired, }; @@ -82,22 +82,22 @@ class ColumnSettings extends PureComponent {

- + - + - + - + diff --git a/app/javascript/mastodon/features/notifications/components/filtered_notifications_banner.jsx b/app/javascript/mastodon/features/notifications/components/filtered_notifications_banner.jsx deleted file mode 100644 index 56da7ba626..0000000000 --- a/app/javascript/mastodon/features/notifications/components/filtered_notifications_banner.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useEffect } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import { useDispatch, useSelector } from 'react-redux'; - -import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react'; -import { fetchNotificationPolicy } from 'mastodon/actions/notifications'; -import { Icon } from 'mastodon/components/icon'; -import { toCappedNumber } from 'mastodon/utils/numbers'; - -export const FilteredNotificationsBanner = () => { - const dispatch = useDispatch(); - const policy = useSelector(state => state.get('notificationPolicy')); - - useEffect(() => { - dispatch(fetchNotificationPolicy()); - - const interval = setInterval(() => { - dispatch(fetchNotificationPolicy()); - }, 120000); - - return () => { - clearInterval(interval); - }; - }, [dispatch]); - - if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) === 0) { - return null; - } - - return ( - - - -
- - -
- -
-
{toCappedNumber(policy.getIn(['summary', 'pending_notifications_count']))}
- -
- - ); -}; diff --git a/app/javascript/mastodon/features/notifications/components/filtered_notifications_banner.tsx b/app/javascript/mastodon/features/notifications/components/filtered_notifications_banner.tsx new file mode 100644 index 0000000000..2c4b3b9717 --- /dev/null +++ b/app/javascript/mastodon/features/notifications/components/filtered_notifications_banner.tsx @@ -0,0 +1,68 @@ +import { useEffect } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react'; +import { fetchNotificationPolicy } from 'mastodon/actions/notification_policies'; +import { Icon } from 'mastodon/components/icon'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; +import { toCappedNumber } from 'mastodon/utils/numbers'; + +export const FilteredNotificationsBanner: React.FC = () => { + const dispatch = useAppDispatch(); + const policy = useAppSelector((state) => state.notificationPolicy); + + useEffect(() => { + void dispatch(fetchNotificationPolicy()); + + const interval = setInterval(() => { + void dispatch(fetchNotificationPolicy()); + }, 120000); + + return () => { + clearInterval(interval); + }; + }, [dispatch]); + + if (policy === null || policy.summary.pending_notifications_count === 0) { + return null; + } + + return ( + + + +
+ + + + + + +
+ +
+
+ {toCappedNumber(policy.summary.pending_notifications_count)} +
+ +
+ + ); +}; diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index de266160f8..94383d0bb5 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -4,7 +4,8 @@ import { connect } from 'react-redux'; import { showAlert } from '../../../actions/alerts'; import { openModal } from '../../../actions/modal'; -import { setFilter, clearNotifications, requestBrowserPermission, updateNotificationsPolicy } from '../../../actions/notifications'; +import { updateNotificationsPolicy } from '../../../actions/notification_policies'; +import { setFilter, clearNotifications, requestBrowserPermission } from '../../../actions/notifications'; import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications'; import { changeSetting } from '../../../actions/settings'; import ColumnSettings from '../components/column_settings'; @@ -15,13 +16,16 @@ const messages = defineMessages({ permissionDenied: { id: 'notifications.permission_denied_alert', defaultMessage: 'Desktop notifications can\'t be enabled, as browser permission has been denied before' }, }); +/** + * @param {import('mastodon/store').RootState} state + */ const mapStateToProps = state => ({ settings: state.getIn(['settings', 'notifications']), pushSettings: state.get('push_notifications'), alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true), browserSupport: state.getIn(['notifications', 'browserSupport']), browserPermission: state.getIn(['notifications', 'browserPermission']), - notificationPolicy: state.get('notificationPolicy'), + notificationPolicy: state.notificationPolicy, }); const mapDispatchToProps = (dispatch, { intl }) => ({ diff --git a/app/javascript/mastodon/models/notification_policy.ts b/app/javascript/mastodon/models/notification_policy.ts new file mode 100644 index 0000000000..eb65403292 --- /dev/null +++ b/app/javascript/mastodon/models/notification_policy.ts @@ -0,0 +1,3 @@ +import type { NotificationPolicyJSON } from 'mastodon/api_types/notification_policies'; + +export type NotificationPolicy = NotificationPolicyJSON; // No changes from the API type diff --git a/app/javascript/mastodon/reducers/notification_policy.js b/app/javascript/mastodon/reducers/notification_policy.js deleted file mode 100644 index 8edb4d12a1..0000000000 --- a/app/javascript/mastodon/reducers/notification_policy.js +++ /dev/null @@ -1,12 +0,0 @@ -import { fromJS } from 'immutable'; - -import { NOTIFICATION_POLICY_FETCH_SUCCESS } from 'mastodon/actions/notifications'; - -export const notificationPolicyReducer = (state = null, action) => { - switch(action.type) { - case NOTIFICATION_POLICY_FETCH_SUCCESS: - return fromJS(action.policy); - default: - return state; - } -}; diff --git a/app/javascript/mastodon/reducers/notification_policy.ts b/app/javascript/mastodon/reducers/notification_policy.ts new file mode 100644 index 0000000000..ab111066cc --- /dev/null +++ b/app/javascript/mastodon/reducers/notification_policy.ts @@ -0,0 +1,18 @@ +import { createReducer, isAnyOf } from '@reduxjs/toolkit'; + +import { + fetchNotificationPolicy, + updateNotificationsPolicy, +} from 'mastodon/actions/notification_policies'; +import type { NotificationPolicy } from 'mastodon/models/notification_policy'; + +export const notificationPolicyReducer = + createReducer(null, (builder) => { + builder.addMatcher( + isAnyOf( + fetchNotificationPolicy.fulfilled, + updateNotificationsPolicy.fulfilled, + ), + (_state, action) => action.payload, + ); + }); diff --git a/app/serializers/rest/notification_policy_serializer.rb b/app/serializers/rest/notification_policy_serializer.rb index a50ba9e66b..8bf85250fa 100644 --- a/app/serializers/rest/notification_policy_serializer.rb +++ b/app/serializers/rest/notification_policy_serializer.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class REST::NotificationPolicySerializer < ActiveModel::Serializer + # Please update `app/javascript/mastodon/api_types/notification_policies.ts` when making changes to the attributes + attributes :filter_not_following, :filter_not_followers, :filter_new_accounts,