From c0eda832f3cc0f4009a59836cd4494e9daeb844c Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 16 Sep 2024 11:54:03 +0200 Subject: [PATCH] Convert notification requests actions and reducers to Typescript (#31866) --- .../mastodon/actions/notification_groups.ts | 8 +- .../mastodon/actions/notification_requests.ts | 234 +++++++++++++ .../mastodon/actions/notifications.js | 328 ------------------ app/javascript/mastodon/api/notifications.ts | 75 +++- .../mastodon/api_types/notifications.ts | 9 + .../components/notification_request.jsx | 6 +- .../features/notifications/request.jsx | 32 +- .../features/notifications/requests.jsx | 33 +- .../mastodon/models/notification_request.ts | 19 + .../reducers/notification_requests.js | 114 ------ .../reducers/notification_requests.ts | 182 ++++++++++ .../mastodon/store/typed_functions.ts | 37 +- 12 files changed, 585 insertions(+), 492 deletions(-) create mode 100644 app/javascript/mastodon/actions/notification_requests.ts create mode 100644 app/javascript/mastodon/models/notification_request.ts delete mode 100644 app/javascript/mastodon/reducers/notification_requests.js create mode 100644 app/javascript/mastodon/reducers/notification_requests.ts diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 2ee46500ab..9d3fc0d425 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -2,7 +2,7 @@ import { createAction } from '@reduxjs/toolkit'; import { apiClearNotifications, - apiFetchNotifications, + apiFetchNotificationGroups, } from 'mastodon/api/notifications'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; import type { @@ -71,7 +71,7 @@ function dispatchAssociatedRecords( export const fetchNotifications = createDataLoadingThunk( 'notificationGroups/fetch', async (_params, { getState }) => - apiFetchNotifications({ exclude_types: getExcludedTypes(getState()) }), + apiFetchNotificationGroups({ exclude_types: getExcludedTypes(getState()) }), ({ notifications, accounts, statuses }, { dispatch }) => { dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedStatuses(statuses)); @@ -92,7 +92,7 @@ export const fetchNotifications = createDataLoadingThunk( export const fetchNotificationsGap = createDataLoadingThunk( 'notificationGroups/fetchGap', async (params: { gap: NotificationGap }, { getState }) => - apiFetchNotifications({ + apiFetchNotificationGroups({ max_id: params.gap.maxId, exclude_types: getExcludedTypes(getState()), }), @@ -108,7 +108,7 @@ export const fetchNotificationsGap = createDataLoadingThunk( export const pollRecentNotifications = createDataLoadingThunk( 'notificationGroups/pollRecentNotifications', async (_params, { getState }) => { - return apiFetchNotifications({ + return apiFetchNotificationGroups({ max_id: undefined, exclude_types: getExcludedTypes(getState()), // In slow mode, we don't want to include notifications that duplicate the already-displayed ones diff --git a/app/javascript/mastodon/actions/notification_requests.ts b/app/javascript/mastodon/actions/notification_requests.ts new file mode 100644 index 0000000000..ef9cbef03e --- /dev/null +++ b/app/javascript/mastodon/actions/notification_requests.ts @@ -0,0 +1,234 @@ +import { + apiFetchNotificationRequest, + apiFetchNotificationRequests, + apiFetchNotifications, + apiAcceptNotificationRequest, + apiDismissNotificationRequest, + apiAcceptNotificationRequests, + apiDismissNotificationRequests, +} from 'mastodon/api/notifications'; +import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; +import type { + ApiNotificationGroupJSON, + ApiNotificationJSON, +} from 'mastodon/api_types/notifications'; +import type { ApiStatusJSON } from 'mastodon/api_types/statuses'; +import type { AppDispatch, RootState } from 'mastodon/store'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +import { importFetchedAccounts, importFetchedStatuses } from './importer'; +import { decreasePendingNotificationsCount } from './notification_policies'; + +// TODO: refactor with notification_groups +function dispatchAssociatedRecords( + dispatch: AppDispatch, + notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[], +) { + const fetchedAccounts: ApiAccountJSON[] = []; + const fetchedStatuses: ApiStatusJSON[] = []; + + notifications.forEach((notification) => { + if (notification.type === 'admin.report') { + fetchedAccounts.push(notification.report.target_account); + } + + if (notification.type === 'moderation_warning') { + fetchedAccounts.push(notification.moderation_warning.target_account); + } + + if ('status' in notification && notification.status) { + fetchedStatuses.push(notification.status); + } + }); + + if (fetchedAccounts.length > 0) + dispatch(importFetchedAccounts(fetchedAccounts)); + + if (fetchedStatuses.length > 0) + dispatch(importFetchedStatuses(fetchedStatuses)); +} + +export const fetchNotificationRequests = createDataLoadingThunk( + 'notificationRequests/fetch', + async (_params, { getState }) => { + let sinceId = undefined; + + if (getState().notificationRequests.items.length > 0) { + sinceId = getState().notificationRequests.items[0]?.id; + } + + return apiFetchNotificationRequests({ + since_id: sinceId, + }); + }, + ({ requests, links }, { dispatch }) => { + const next = links.refs.find((link) => link.rel === 'next'); + + dispatch(importFetchedAccounts(requests.map((request) => request.account))); + + return { requests, next: next?.uri }; + }, + { + condition: (_params, { getState }) => + !getState().notificationRequests.isLoading, + }, +); + +export const fetchNotificationRequest = createDataLoadingThunk( + 'notificationRequest/fetch', + async ({ id }: { id: string }) => apiFetchNotificationRequest(id), + { + condition: ({ id }, { getState }) => + !( + getState().notificationRequests.current.item?.id === id || + getState().notificationRequests.current.isLoading + ), + }, +); + +export const expandNotificationRequests = createDataLoadingThunk( + 'notificationRequests/expand', + async (_, { getState }) => { + const nextUrl = getState().notificationRequests.next; + if (!nextUrl) throw new Error('missing URL'); + + return apiFetchNotificationRequests(undefined, nextUrl); + }, + ({ requests, links }, { dispatch }) => { + const next = links.refs.find((link) => link.rel === 'next'); + + dispatch(importFetchedAccounts(requests.map((request) => request.account))); + + return { requests, next: next?.uri }; + }, + { + condition: (_, { getState }) => + !!getState().notificationRequests.next && + !getState().notificationRequests.isLoading, + }, +); + +export const fetchNotificationsForRequest = createDataLoadingThunk( + 'notificationRequest/fetchNotifications', + async ({ accountId }: { accountId: string }, { getState }) => { + const sinceId = + // @ts-expect-error current.notifications.items is not yet typed + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + getState().notificationRequests.current.notifications.items[0]?.get( + 'id', + ) as string | undefined; + + return apiFetchNotifications({ + since_id: sinceId, + account_id: accountId, + }); + }, + ({ notifications, links }, { dispatch }) => { + const next = links.refs.find((link) => link.rel === 'next'); + + dispatchAssociatedRecords(dispatch, notifications); + + return { notifications, next: next?.uri }; + }, + { + condition: ({ accountId }, { getState }) => { + const current = getState().notificationRequests.current; + return !( + current.item?.account_id === accountId && + current.notifications.isLoading + ); + }, + }, +); + +export const expandNotificationsForRequest = createDataLoadingThunk( + 'notificationRequest/expandNotifications', + async (_, { getState }) => { + const nextUrl = getState().notificationRequests.current.notifications.next; + if (!nextUrl) throw new Error('missing URL'); + + return apiFetchNotifications(undefined, nextUrl); + }, + ({ notifications, links }, { dispatch }) => { + const next = links.refs.find((link) => link.rel === 'next'); + + dispatchAssociatedRecords(dispatch, notifications); + + return { notifications, next: next?.uri }; + }, + { + condition: ({ accountId }: { accountId: string }, { getState }) => { + const url = getState().notificationRequests.current.notifications.next; + + return ( + !!url && + !getState().notificationRequests.current.notifications.isLoading && + getState().notificationRequests.current.item?.account_id === accountId + ); + }, + }, +); + +const selectNotificationCountForRequest = (state: RootState, id: string) => { + const requests = state.notificationRequests.items; + const thisRequest = requests.find((request) => request.id === id); + return thisRequest ? thisRequest.notifications_count : 0; +}; + +export const acceptNotificationRequest = createDataLoadingThunk( + 'notificationRequest/accept', + ({ id }: { id: string }) => apiAcceptNotificationRequest(id), + (_data, { dispatch, getState, discardLoadData, actionArg: { id } }) => { + const count = selectNotificationCountForRequest(getState(), id); + + dispatch(decreasePendingNotificationsCount(count)); + + // The payload is not used in any functions + return discardLoadData; + }, +); + +export const dismissNotificationRequest = createDataLoadingThunk( + 'notificationRequest/dismiss', + ({ id }: { id: string }) => apiDismissNotificationRequest(id), + (_data, { dispatch, getState, discardLoadData, actionArg: { id } }) => { + const count = selectNotificationCountForRequest(getState(), id); + + dispatch(decreasePendingNotificationsCount(count)); + + // The payload is not used in any functions + return discardLoadData; + }, +); + +export const acceptNotificationRequests = createDataLoadingThunk( + 'notificationRequests/acceptBulk', + ({ ids }: { ids: string[] }) => apiAcceptNotificationRequests(ids), + (_data, { dispatch, getState, discardLoadData, actionArg: { ids } }) => { + const count = ids.reduce( + (count, id) => count + selectNotificationCountForRequest(getState(), id), + 0, + ); + + dispatch(decreasePendingNotificationsCount(count)); + + // The payload is not used in any functions + return discardLoadData; + }, +); + +export const dismissNotificationRequests = createDataLoadingThunk( + 'notificationRequests/dismissBulk', + ({ ids }: { ids: string[] }) => apiDismissNotificationRequests(ids), + (_data, { dispatch, getState, discardLoadData, actionArg: { ids } }) => { + const count = ids.reduce( + (count, id) => count + selectNotificationCountForRequest(getState(), id), + 0, + ); + + dispatch(decreasePendingNotificationsCount(count)); + + // The payload is not used in any functions + return discardLoadData; + }, +); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index f5105d460f..14072562d4 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -18,7 +18,6 @@ import { importFetchedStatuses, } from './importer'; import { submitMarkers } from './markers'; -import { decreasePendingNotificationsCount } from './notification_policies'; import { notificationsUpdate } from "./notifications_typed"; import { register as registerPushNotifications } from './push_notifications'; import { saveSettings } from './settings'; @@ -44,26 +43,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_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'; - -export const NOTIFICATION_REQUESTS_EXPAND_REQUEST = 'NOTIFICATION_REQUESTS_EXPAND_REQUEST'; -export const NOTIFICATION_REQUESTS_EXPAND_SUCCESS = 'NOTIFICATION_REQUESTS_EXPAND_SUCCESS'; -export const NOTIFICATION_REQUESTS_EXPAND_FAIL = 'NOTIFICATION_REQUESTS_EXPAND_FAIL'; - -export const NOTIFICATION_REQUEST_FETCH_REQUEST = 'NOTIFICATION_REQUEST_FETCH_REQUEST'; -export const NOTIFICATION_REQUEST_FETCH_SUCCESS = 'NOTIFICATION_REQUEST_FETCH_SUCCESS'; -export const NOTIFICATION_REQUEST_FETCH_FAIL = 'NOTIFICATION_REQUEST_FETCH_FAIL'; - -export const NOTIFICATION_REQUEST_ACCEPT_REQUEST = 'NOTIFICATION_REQUEST_ACCEPT_REQUEST'; -export const NOTIFICATION_REQUEST_ACCEPT_SUCCESS = 'NOTIFICATION_REQUEST_ACCEPT_SUCCESS'; -export const NOTIFICATION_REQUEST_ACCEPT_FAIL = 'NOTIFICATION_REQUEST_ACCEPT_FAIL'; - -export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMISS_REQUEST'; -export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS'; -export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL'; - export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST'; export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS'; export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL'; @@ -72,14 +51,6 @@ export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISM export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS'; export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL'; -export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST'; -export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS'; -export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL'; - -export const NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST'; -export const NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS'; -export const NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL'; - defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, @@ -93,12 +64,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => { } }; -const selectNotificationCountForRequest = (state, id) => { - const requests = state.getIn(['notificationRequests', 'items']); - const thisRequest = requests.find(request => request.get('id') === id); - return thisRequest ? thisRequest.get('notifications_count') : 0; -}; - export const loadPending = () => ({ type: NOTIFICATIONS_LOAD_PENDING, }); @@ -343,296 +308,3 @@ export function setBrowserPermission (value) { value, }; } - -export const fetchNotificationRequests = () => (dispatch, getState) => { - const params = {}; - - if (getState().getIn(['notificationRequests', 'isLoading'])) { - return; - } - - if (getState().getIn(['notificationRequests', 'items'])?.size > 0) { - params.since_id = getState().getIn(['notificationRequests', 'items', 0, 'id']); - } - - dispatch(fetchNotificationRequestsRequest()); - - api().get('/api/v1/notifications/requests', { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data.map(x => x.account))); - dispatch(fetchNotificationRequestsSuccess(response.data, next ? next.uri : null)); - }).catch(err => { - dispatch(fetchNotificationRequestsFail(err)); - }); -}; - -export const fetchNotificationRequestsRequest = () => ({ - type: NOTIFICATION_REQUESTS_FETCH_REQUEST, -}); - -export const fetchNotificationRequestsSuccess = (requests, next) => ({ - type: NOTIFICATION_REQUESTS_FETCH_SUCCESS, - requests, - next, -}); - -export const fetchNotificationRequestsFail = error => ({ - type: NOTIFICATION_REQUESTS_FETCH_FAIL, - error, -}); - -export const expandNotificationRequests = () => (dispatch, getState) => { - const url = getState().getIn(['notificationRequests', 'next']); - - if (!url || getState().getIn(['notificationRequests', 'isLoading'])) { - return; - } - - dispatch(expandNotificationRequestsRequest()); - - api().get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data.map(x => x.account))); - dispatch(expandNotificationRequestsSuccess(response.data, next?.uri)); - }).catch(err => { - dispatch(expandNotificationRequestsFail(err)); - }); -}; - -export const expandNotificationRequestsRequest = () => ({ - type: NOTIFICATION_REQUESTS_EXPAND_REQUEST, -}); - -export const expandNotificationRequestsSuccess = (requests, next) => ({ - type: NOTIFICATION_REQUESTS_EXPAND_SUCCESS, - requests, - next, -}); - -export const expandNotificationRequestsFail = error => ({ - type: NOTIFICATION_REQUESTS_EXPAND_FAIL, - error, -}); - -export const fetchNotificationRequest = id => (dispatch, getState) => { - const current = getState().getIn(['notificationRequests', 'current']); - - if (current.getIn(['item', 'id']) === id || current.get('isLoading')) { - return; - } - - dispatch(fetchNotificationRequestRequest(id)); - - api().get(`/api/v1/notifications/requests/${id}`).then(({ data }) => { - dispatch(fetchNotificationRequestSuccess(data)); - }).catch(err => { - dispatch(fetchNotificationRequestFail(id, err)); - }); -}; - -export const fetchNotificationRequestRequest = id => ({ - type: NOTIFICATION_REQUEST_FETCH_REQUEST, - id, -}); - -export const fetchNotificationRequestSuccess = request => ({ - type: NOTIFICATION_REQUEST_FETCH_SUCCESS, - request, -}); - -export const fetchNotificationRequestFail = (id, error) => ({ - type: NOTIFICATION_REQUEST_FETCH_FAIL, - id, - error, -}); - -export const acceptNotificationRequest = (id) => (dispatch, getState) => { - const count = selectNotificationCountForRequest(getState(), id); - dispatch(acceptNotificationRequestRequest(id)); - - api().post(`/api/v1/notifications/requests/${id}/accept`).then(() => { - dispatch(acceptNotificationRequestSuccess(id)); - dispatch(decreasePendingNotificationsCount(count)); - }).catch(err => { - dispatch(acceptNotificationRequestFail(id, err)); - }); -}; - -export const acceptNotificationRequestRequest = id => ({ - type: NOTIFICATION_REQUEST_ACCEPT_REQUEST, - id, -}); - -export const acceptNotificationRequestSuccess = id => ({ - type: NOTIFICATION_REQUEST_ACCEPT_SUCCESS, - id, -}); - -export const acceptNotificationRequestFail = (id, error) => ({ - type: NOTIFICATION_REQUEST_ACCEPT_FAIL, - id, - error, -}); - -export const dismissNotificationRequest = (id) => (dispatch, getState) => { - const count = selectNotificationCountForRequest(getState(), id); - dispatch(dismissNotificationRequestRequest(id)); - - api().post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{ - dispatch(dismissNotificationRequestSuccess(id)); - dispatch(decreasePendingNotificationsCount(count)); - }).catch(err => { - dispatch(dismissNotificationRequestFail(id, err)); - }); -}; - -export const dismissNotificationRequestRequest = id => ({ - type: NOTIFICATION_REQUEST_DISMISS_REQUEST, - id, -}); - -export const dismissNotificationRequestSuccess = id => ({ - type: NOTIFICATION_REQUEST_DISMISS_SUCCESS, - id, -}); - -export const dismissNotificationRequestFail = (id, error) => ({ - type: NOTIFICATION_REQUEST_DISMISS_FAIL, - id, - error, -}); - -export const acceptNotificationRequests = (ids) => (dispatch, getState) => { - const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0); - dispatch(acceptNotificationRequestsRequest(ids)); - - api().post(`/api/v1/notifications/requests/accept`, { id: ids }).then(() => { - dispatch(acceptNotificationRequestsSuccess(ids)); - dispatch(decreasePendingNotificationsCount(count)); - }).catch(err => { - dispatch(acceptNotificationRequestFail(ids, err)); - }); -}; - -export const acceptNotificationRequestsRequest = ids => ({ - type: NOTIFICATION_REQUESTS_ACCEPT_REQUEST, - ids, -}); - -export const acceptNotificationRequestsSuccess = ids => ({ - type: NOTIFICATION_REQUESTS_ACCEPT_SUCCESS, - ids, -}); - -export const acceptNotificationRequestsFail = (ids, error) => ({ - type: NOTIFICATION_REQUESTS_ACCEPT_FAIL, - ids, - error, -}); - -export const dismissNotificationRequests = (ids) => (dispatch, getState) => { - const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0); - dispatch(acceptNotificationRequestsRequest(ids)); - - api().post(`/api/v1/notifications/requests/dismiss`, { id: ids }).then(() => { - dispatch(dismissNotificationRequestsSuccess(ids)); - dispatch(decreasePendingNotificationsCount(count)); - }).catch(err => { - dispatch(dismissNotificationRequestFail(ids, err)); - }); -}; - -export const dismissNotificationRequestsRequest = ids => ({ - type: NOTIFICATION_REQUESTS_DISMISS_REQUEST, - ids, -}); - -export const dismissNotificationRequestsSuccess = ids => ({ - type: NOTIFICATION_REQUESTS_DISMISS_SUCCESS, - ids, -}); - -export const dismissNotificationRequestsFail = (ids, error) => ({ - type: NOTIFICATION_REQUESTS_DISMISS_FAIL, - ids, - error, -}); - -export const fetchNotificationsForRequest = accountId => (dispatch, getState) => { - const current = getState().getIn(['notificationRequests', 'current']); - const params = { account_id: accountId }; - - if (current.getIn(['item', 'account']) === accountId) { - if (current.getIn(['notifications', 'isLoading'])) { - return; - } - - if (current.getIn(['notifications', 'items'])?.size > 0) { - params.since_id = current.getIn(['notifications', 'items', 0, 'id']); - } - } - - dispatch(fetchNotificationsForRequestRequest()); - - api().get('/api/v1/notifications', { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data.map(item => item.account))); - dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); - - dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri)); - }).catch(err => { - dispatch(fetchNotificationsForRequestFail(err)); - }); -}; - -export const fetchNotificationsForRequestRequest = () => ({ - type: NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST, -}); - -export const fetchNotificationsForRequestSuccess = (notifications, next) => ({ - type: NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS, - notifications, - next, -}); - -export const fetchNotificationsForRequestFail = (error) => ({ - type: NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL, - error, -}); - -export const expandNotificationsForRequest = () => (dispatch, getState) => { - const url = getState().getIn(['notificationRequests', 'current', 'notifications', 'next']); - - if (!url || getState().getIn(['notificationRequests', 'current', 'notifications', 'isLoading'])) { - return; - } - - dispatch(expandNotificationsForRequestRequest()); - - api().get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedAccounts(response.data.map(item => item.account))); - dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); - - dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri)); - }).catch(err => { - dispatch(expandNotificationsForRequestFail(err)); - }); -}; - -export const expandNotificationsForRequestRequest = () => ({ - type: NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST, -}); - -export const expandNotificationsForRequestSuccess = (notifications, next) => ({ - type: NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS, - notifications, - next, -}); - -export const expandNotificationsForRequestFail = (error) => ({ - type: NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL, - error, -}); diff --git a/app/javascript/mastodon/api/notifications.ts b/app/javascript/mastodon/api/notifications.ts index cb07e4114c..e34dc85bb0 100644 --- a/app/javascript/mastodon/api/notifications.ts +++ b/app/javascript/mastodon/api/notifications.ts @@ -1,7 +1,36 @@ -import api, { apiRequest, getLinks } from 'mastodon/api'; -import type { ApiNotificationGroupsResultJSON } from 'mastodon/api_types/notifications'; +import api, { + apiRequest, + getLinks, + apiRequestGet, + apiRequestPost, +} from 'mastodon/api'; +import type { + ApiNotificationGroupsResultJSON, + ApiNotificationRequestJSON, + ApiNotificationJSON, +} from 'mastodon/api_types/notifications'; -export const apiFetchNotifications = async (params?: { +export const apiFetchNotifications = async ( + params?: { + account_id?: string; + since_id?: string; + }, + url?: string, +) => { + const response = await api().request({ + method: 'GET', + url: url ?? '/api/v1/notifications', + params, + }); + + return { + notifications: response.data, + links: getLinks(response), + }; +}; + +export const apiFetchNotificationGroups = async (params?: { + url?: string; exclude_types?: string[]; max_id?: string; since_id?: string; @@ -24,3 +53,43 @@ export const apiFetchNotifications = async (params?: { export const apiClearNotifications = () => apiRequest('POST', 'v1/notifications/clear'); + +export const apiFetchNotificationRequests = async ( + params?: { + since_id?: string; + }, + url?: string, +) => { + const response = await api().request({ + method: 'GET', + url: url ?? '/api/v1/notifications/requests', + params, + }); + + return { + requests: response.data, + links: getLinks(response), + }; +}; + +export const apiFetchNotificationRequest = async (id: string) => { + return apiRequestGet( + `v1/notifications/requests/${id}`, + ); +}; + +export const apiAcceptNotificationRequest = async (id: string) => { + return apiRequestPost(`v1/notifications/requests/${id}/accept`); +}; + +export const apiDismissNotificationRequest = async (id: string) => { + return apiRequestPost(`v1/notifications/requests/${id}/dismiss`); +}; + +export const apiAcceptNotificationRequests = async (id: string[]) => { + return apiRequestPost('v1/notifications/requests/accept', { id }); +}; + +export const apiDismissNotificationRequests = async (id: string[]) => { + return apiRequestPost('v1/notifications/dismiss/dismiss', { id }); +}; diff --git a/app/javascript/mastodon/api_types/notifications.ts b/app/javascript/mastodon/api_types/notifications.ts index 4ab9a4c90a..28ba7eb5c2 100644 --- a/app/javascript/mastodon/api_types/notifications.ts +++ b/app/javascript/mastodon/api_types/notifications.ts @@ -149,3 +149,12 @@ export interface ApiNotificationGroupsResultJSON { statuses: ApiStatusJSON[]; notification_groups: ApiNotificationGroupJSON[]; } + +export interface ApiNotificationRequestJSON { + id: string; + created_at: string; + updated_at: string; + notifications_count: string; + account: ApiAccountJSON; + last_status?: ApiStatusJSON; +} diff --git a/app/javascript/mastodon/features/notifications/components/notification_request.jsx b/app/javascript/mastodon/features/notifications/components/notification_request.jsx index 2f378942bc..626929ae50 100644 --- a/app/javascript/mastodon/features/notifications/components/notification_request.jsx +++ b/app/javascript/mastodon/features/notifications/components/notification_request.jsx @@ -12,7 +12,7 @@ import DeleteIcon from '@/material-icons/400-24px/delete.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import { initBlockModal } from 'mastodon/actions/blocks'; import { initMuteModal } from 'mastodon/actions/mutes'; -import { acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications'; +import { acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notification_requests'; import { initReport } from 'mastodon/actions/reports'; import { Avatar } from 'mastodon/components/avatar'; import { CheckBox } from 'mastodon/components/check_box'; @@ -40,11 +40,11 @@ export const NotificationRequest = ({ id, accountId, notificationsCount, checked const { push: historyPush } = useHistory(); const handleDismiss = useCallback(() => { - dispatch(dismissNotificationRequest(id)); + dispatch(dismissNotificationRequest({ id })); }, [dispatch, id]); const handleAccept = useCallback(() => { - dispatch(acceptNotificationRequest(id)); + dispatch(acceptNotificationRequest({ id })); }, [dispatch, id]); const handleMute = useCallback(() => { diff --git a/app/javascript/mastodon/features/notifications/request.jsx b/app/javascript/mastodon/features/notifications/request.jsx index 30ec004e70..a7e5180a4f 100644 --- a/app/javascript/mastodon/features/notifications/request.jsx +++ b/app/javascript/mastodon/features/notifications/request.jsx @@ -10,7 +10,13 @@ import { useSelector, useDispatch } from 'react-redux'; import DeleteIcon from '@/material-icons/400-24px/delete.svg?react'; import DoneIcon from '@/material-icons/400-24px/done.svg?react'; import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react'; -import { fetchNotificationRequest, fetchNotificationsForRequest, expandNotificationsForRequest, acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications'; +import { + fetchNotificationRequest, + fetchNotificationsForRequest, + expandNotificationsForRequest, + acceptNotificationRequest, + dismissNotificationRequest, +} from 'mastodon/actions/notification_requests'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; import { IconButton } from 'mastodon/components/icon_button'; @@ -44,28 +50,28 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => { const columnRef = useRef(); const intl = useIntl(); const dispatch = useDispatch(); - const notificationRequest = useSelector(state => state.getIn(['notificationRequests', 'current', 'item', 'id']) === id ? state.getIn(['notificationRequests', 'current', 'item']) : null); - const accountId = notificationRequest?.get('account'); + const notificationRequest = useSelector(state => state.notificationRequests.current.item?.id === id ? state.notificationRequests.current.item : null); + const accountId = notificationRequest?.account_id; const account = useSelector(state => state.getIn(['accounts', accountId])); - const notifications = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'items'])); - const isLoading = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'isLoading'])); - const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'current', 'notifications', 'next'])); - const removed = useSelector(state => state.getIn(['notificationRequests', 'current', 'removed'])); + const notifications = useSelector(state => state.notificationRequests.current.notifications.items); + const isLoading = useSelector(state => state.notificationRequests.current.notifications.isLoading); + const hasMore = useSelector(state => !!state.notificationRequests.current.notifications.next); + const removed = useSelector(state => state.notificationRequests.current.removed); const handleHeaderClick = useCallback(() => { columnRef.current?.scrollTop(); }, [columnRef]); const handleLoadMore = useCallback(() => { - dispatch(expandNotificationsForRequest()); - }, [dispatch]); + dispatch(expandNotificationsForRequest({ accountId })); + }, [dispatch, accountId]); const handleDismiss = useCallback(() => { - dispatch(dismissNotificationRequest(id)); + dispatch(dismissNotificationRequest({ id })); }, [dispatch, id]); const handleAccept = useCallback(() => { - dispatch(acceptNotificationRequest(id)); + dispatch(acceptNotificationRequest({ id })); }, [dispatch, id]); const handleMoveUp = useCallback(id => { @@ -79,12 +85,12 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => { }, [columnRef, notifications]); useEffect(() => { - dispatch(fetchNotificationRequest(id)); + dispatch(fetchNotificationRequest({ id })); }, [dispatch, id]); useEffect(() => { if (accountId) { - dispatch(fetchNotificationsForRequest(accountId)); + dispatch(fetchNotificationsForRequest({ accountId })); } }, [dispatch, accountId]); diff --git a/app/javascript/mastodon/features/notifications/requests.jsx b/app/javascript/mastodon/features/notifications/requests.jsx index 622a171fd3..ccaed312b4 100644 --- a/app/javascript/mastodon/features/notifications/requests.jsx +++ b/app/javascript/mastodon/features/notifications/requests.jsx @@ -11,7 +11,12 @@ import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?rea import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import { openModal } from 'mastodon/actions/modal'; -import { fetchNotificationRequests, expandNotificationRequests, acceptNotificationRequests, dismissNotificationRequests } from 'mastodon/actions/notifications'; +import { + fetchNotificationRequests, + expandNotificationRequests, + acceptNotificationRequests, + dismissNotificationRequests, +} from 'mastodon/actions/notification_requests'; import { changeSetting } from 'mastodon/actions/settings'; import { CheckBox } from 'mastodon/components/check_box'; import Column from 'mastodon/components/column'; @@ -84,7 +89,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM message: intl.formatMessage(messages.confirmAcceptMultipleMessage, { count: selectedItems.length }), confirm: intl.formatMessage(messages.confirmAcceptMultipleButton, { count: selectedItems.length}), onConfirm: () => - dispatch(acceptNotificationRequests(selectedItems)), + dispatch(acceptNotificationRequests({ ids: selectedItems })), }, })); }, [dispatch, intl, selectedItems]); @@ -97,7 +102,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM message: intl.formatMessage(messages.confirmDismissMultipleMessage, { count: selectedItems.length }), confirm: intl.formatMessage(messages.confirmDismissMultipleButton, { count: selectedItems.length}), onConfirm: () => - dispatch(dismissNotificationRequests(selectedItems)), + dispatch(dismissNotificationRequests({ ids: selectedItems })), }, })); }, [dispatch, intl, selectedItems]); @@ -161,9 +166,9 @@ export const NotificationRequests = ({ multiColumn }) => { const columnRef = useRef(); const intl = useIntl(); const dispatch = useDispatch(); - const isLoading = useSelector(state => state.getIn(['notificationRequests', 'isLoading'])); - const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items'])); - const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'next'])); + const isLoading = useSelector(state => state.notificationRequests.isLoading); + const notificationRequests = useSelector(state => state.notificationRequests.items); + const hasMore = useSelector(state => !!state.notificationRequests.next); const [selectionMode, setSelectionMode] = useState(false); const [checkedRequestIds, setCheckedRequestIds] = useState([]); @@ -182,7 +187,7 @@ export const NotificationRequests = ({ multiColumn }) => { else ids.push(id); - setSelectAllChecked(ids.length === notificationRequests.size); + setSelectAllChecked(ids.length === notificationRequests.length); return [...ids]; }); @@ -193,7 +198,7 @@ export const NotificationRequests = ({ multiColumn }) => { if(checked) setCheckedRequestIds([]); else - setCheckedRequestIds(notificationRequests.map(request => request.get('id')).toArray()); + setCheckedRequestIds(notificationRequests.map(request => request.id)); return !checked; }); @@ -217,7 +222,7 @@ export const NotificationRequests = ({ multiColumn }) => { multiColumn={multiColumn} showBackButton appendContent={ - notificationRequests.size > 0 && ( + notificationRequests.length > 0 && ( )} > @@ -236,12 +241,12 @@ export const NotificationRequests = ({ multiColumn }) => { > {notificationRequests.map(request => ( ))} diff --git a/app/javascript/mastodon/models/notification_request.ts b/app/javascript/mastodon/models/notification_request.ts new file mode 100644 index 0000000000..bd98d213d4 --- /dev/null +++ b/app/javascript/mastodon/models/notification_request.ts @@ -0,0 +1,19 @@ +import type { ApiNotificationRequestJSON } from 'mastodon/api_types/notifications'; + +export interface NotificationRequest + extends Omit { + account_id: string; + notifications_count: number; +} + +export function createNotificationRequestFromJSON( + requestJSON: ApiNotificationRequestJSON, +): NotificationRequest { + const { account, notifications_count, ...request } = requestJSON; + + return { + account_id: account.id, + notifications_count: +notifications_count, + ...request, + }; +} diff --git a/app/javascript/mastodon/reducers/notification_requests.js b/app/javascript/mastodon/reducers/notification_requests.js deleted file mode 100644 index f73c641965..0000000000 --- a/app/javascript/mastodon/reducers/notification_requests.js +++ /dev/null @@ -1,114 +0,0 @@ -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; - -import { blockAccountSuccess, muteAccountSuccess } from 'mastodon/actions/accounts'; -import { - NOTIFICATION_REQUESTS_EXPAND_REQUEST, - NOTIFICATION_REQUESTS_EXPAND_SUCCESS, - NOTIFICATION_REQUESTS_EXPAND_FAIL, - NOTIFICATION_REQUESTS_FETCH_REQUEST, - NOTIFICATION_REQUESTS_FETCH_SUCCESS, - NOTIFICATION_REQUESTS_FETCH_FAIL, - NOTIFICATION_REQUEST_FETCH_REQUEST, - NOTIFICATION_REQUEST_FETCH_SUCCESS, - NOTIFICATION_REQUEST_FETCH_FAIL, - NOTIFICATION_REQUEST_ACCEPT_REQUEST, - NOTIFICATION_REQUEST_DISMISS_REQUEST, - NOTIFICATION_REQUESTS_ACCEPT_REQUEST, - NOTIFICATION_REQUESTS_DISMISS_REQUEST, - NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST, - NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS, - NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL, - NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST, - NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS, - NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL, -} from 'mastodon/actions/notifications'; - -import { notificationToMap } from './notifications'; - -const initialState = ImmutableMap({ - items: ImmutableList(), - isLoading: false, - next: null, - current: ImmutableMap({ - isLoading: false, - item: null, - removed: false, - notifications: ImmutableMap({ - items: ImmutableList(), - isLoading: false, - next: null, - }), - }), -}); - -const normalizeRequest = request => fromJS({ - ...request, - account: request.account.id, -}); - -const removeRequest = (state, id) => { - if (state.getIn(['current', 'item', 'id']) === id) { - state = state.setIn(['current', 'removed'], true); - } - - return state.update('items', list => list.filterNot(item => item.get('id') === id)); -}; - -const removeRequestByAccount = (state, account_id) => { - if (state.getIn(['current', 'item', 'account']) === account_id) { - state = state.setIn(['current', 'removed'], true); - } - - return state.update('items', list => list.filterNot(item => item.get('account') === account_id)); -}; - -export const notificationRequestsReducer = (state = initialState, action) => { - switch(action.type) { - case NOTIFICATION_REQUESTS_FETCH_SUCCESS: - return state.withMutations(map => { - map.update('items', list => ImmutableList(action.requests.map(normalizeRequest)).concat(list)); - map.set('isLoading', false); - map.update('next', next => next ?? action.next); - }); - case NOTIFICATION_REQUESTS_EXPAND_SUCCESS: - return state.withMutations(map => { - map.update('items', list => list.concat(ImmutableList(action.requests.map(normalizeRequest)))); - map.set('isLoading', false); - map.set('next', action.next); - }); - case NOTIFICATION_REQUESTS_EXPAND_REQUEST: - case NOTIFICATION_REQUESTS_FETCH_REQUEST: - return state.set('isLoading', true); - case NOTIFICATION_REQUESTS_EXPAND_FAIL: - case NOTIFICATION_REQUESTS_FETCH_FAIL: - return state.set('isLoading', false); - case NOTIFICATION_REQUEST_ACCEPT_REQUEST: - case NOTIFICATION_REQUEST_DISMISS_REQUEST: - return removeRequest(state, action.id); - case NOTIFICATION_REQUESTS_ACCEPT_REQUEST: - case NOTIFICATION_REQUESTS_DISMISS_REQUEST: - return action.ids.reduce((state, id) => removeRequest(state, id), state); - case blockAccountSuccess.type: - return removeRequestByAccount(state, action.payload.relationship.id); - case muteAccountSuccess.type: - return action.payload.relationship.muting_notifications ? removeRequestByAccount(state, action.payload.relationship.id) : state; - case NOTIFICATION_REQUEST_FETCH_REQUEST: - return state.set('current', initialState.get('current').set('isLoading', true)); - case NOTIFICATION_REQUEST_FETCH_SUCCESS: - return state.update('current', map => map.set('isLoading', false).set('item', normalizeRequest(action.request))); - case NOTIFICATION_REQUEST_FETCH_FAIL: - return state.update('current', map => map.set('isLoading', false)); - case NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST: - case NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST: - return state.setIn(['current', 'notifications', 'isLoading'], true); - case NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS: - return state.updateIn(['current', 'notifications'], map => map.set('isLoading', false).update('items', list => ImmutableList(action.notifications.map(notificationToMap)).concat(list)).update('next', next => next ?? action.next)); - case NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS: - return state.updateIn(['current', 'notifications'], map => map.set('isLoading', false).update('items', list => list.concat(ImmutableList(action.notifications.map(notificationToMap)))).set('next', action.next)); - case NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL: - case NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL: - return state.setIn(['current', 'notifications', 'isLoading'], false); - default: - return state; - } -}; diff --git a/app/javascript/mastodon/reducers/notification_requests.ts b/app/javascript/mastodon/reducers/notification_requests.ts new file mode 100644 index 0000000000..1ca92d9990 --- /dev/null +++ b/app/javascript/mastodon/reducers/notification_requests.ts @@ -0,0 +1,182 @@ +import { createReducer, isAnyOf } from '@reduxjs/toolkit'; + +import { + blockAccountSuccess, + muteAccountSuccess, +} from 'mastodon/actions/accounts'; +import { + fetchNotificationRequests, + expandNotificationRequests, + fetchNotificationRequest, + fetchNotificationsForRequest, + expandNotificationsForRequest, + acceptNotificationRequest, + dismissNotificationRequest, + acceptNotificationRequests, + dismissNotificationRequests, +} from 'mastodon/actions/notification_requests'; +import type { NotificationRequest } from 'mastodon/models/notification_request'; +import { createNotificationRequestFromJSON } from 'mastodon/models/notification_request'; + +import { notificationToMap } from './notifications'; + +interface NotificationsListState { + items: unknown[]; // TODO + isLoading: boolean; + next: string | null; +} + +interface CurrentNotificationRequestState { + item: NotificationRequest | null; + isLoading: boolean; + removed: boolean; + notifications: NotificationsListState; +} + +interface NotificationRequestsState { + items: NotificationRequest[]; + isLoading: boolean; + next: string | null; + current: CurrentNotificationRequestState; +} + +const initialState: NotificationRequestsState = { + items: [], + isLoading: false, + next: null, + current: { + item: null, + isLoading: false, + removed: false, + notifications: { + isLoading: false, + items: [], + next: null, + }, + }, +}; + +const removeRequest = (state: NotificationRequestsState, id: string) => { + if (state.current.item?.id === id) { + state.current.removed = true; + } + + state.items = state.items.filter((item) => item.id !== id); +}; + +const removeRequestByAccount = ( + state: NotificationRequestsState, + account_id: string, +) => { + if (state.current.item?.account_id === account_id) { + state.current.removed = true; + } + + state.items = state.items.filter((item) => item.account_id !== account_id); +}; + +export const notificationRequestsReducer = + createReducer(initialState, (builder) => { + builder + .addCase(fetchNotificationRequests.fulfilled, (state, action) => { + state.items = action.payload.requests + .map(createNotificationRequestFromJSON) + .concat(state.items); + state.isLoading = false; + state.next ??= action.payload.next ?? null; + }) + .addCase(expandNotificationRequests.fulfilled, (state, action) => { + state.items = state.items.concat( + action.payload.requests.map(createNotificationRequestFromJSON), + ); + state.isLoading = false; + state.next = action.payload.next ?? null; + }) + .addCase(blockAccountSuccess, (state, action) => { + removeRequestByAccount(state, action.payload.relationship.id); + }) + .addCase(muteAccountSuccess, (state, action) => { + if (action.payload.relationship.muting_notifications) + removeRequestByAccount(state, action.payload.relationship.id); + }) + .addCase(fetchNotificationRequest.pending, (state) => { + state.current = { ...initialState.current, isLoading: true }; + }) + .addCase(fetchNotificationRequest.rejected, (state) => { + state.current.isLoading = false; + }) + .addCase(fetchNotificationRequest.fulfilled, (state, action) => { + state.current.isLoading = false; + state.current.item = createNotificationRequestFromJSON(action.payload); + }) + .addCase(fetchNotificationsForRequest.fulfilled, (state, action) => { + state.current.notifications.isLoading = false; + state.current.notifications.items.unshift( + ...action.payload.notifications.map(notificationToMap), + ); + state.current.notifications.next ??= action.payload.next ?? null; + }) + .addCase(expandNotificationsForRequest.fulfilled, (state, action) => { + state.current.notifications.isLoading = false; + state.current.notifications.items.push( + ...action.payload.notifications.map(notificationToMap), + ); + state.current.notifications.next = action.payload.next ?? null; + }) + .addMatcher( + isAnyOf( + fetchNotificationRequests.pending, + expandNotificationRequests.pending, + ), + (state) => { + state.isLoading = true; + }, + ) + .addMatcher( + isAnyOf( + fetchNotificationRequests.rejected, + expandNotificationRequests.rejected, + ), + (state) => { + state.isLoading = false; + }, + ) + .addMatcher( + isAnyOf( + acceptNotificationRequest.pending, + dismissNotificationRequest.pending, + ), + (state, action) => { + removeRequest(state, action.meta.arg.id); + }, + ) + .addMatcher( + isAnyOf( + acceptNotificationRequests.pending, + dismissNotificationRequests.pending, + ), + (state, action) => { + action.meta.arg.ids.forEach((id) => { + removeRequest(state, id); + }); + }, + ) + .addMatcher( + isAnyOf( + fetchNotificationsForRequest.pending, + expandNotificationsForRequest.pending, + ), + (state) => { + state.current.notifications.isLoading = true; + }, + ) + .addMatcher( + isAnyOf( + fetchNotificationsForRequest.rejected, + expandNotificationsForRequest.rejected, + ), + (state) => { + state.current.notifications.isLoading = false; + }, + ); + }); diff --git a/app/javascript/mastodon/store/typed_functions.ts b/app/javascript/mastodon/store/typed_functions.ts index 544d619682..9fcc90c61b 100644 --- a/app/javascript/mastodon/store/typed_functions.ts +++ b/app/javascript/mastodon/store/typed_functions.ts @@ -33,8 +33,12 @@ interface AppThunkConfig { } type AppThunkApi = Pick, 'getState' | 'dispatch'>; -interface AppThunkOptions { +interface AppThunkOptions { useLoadingBar?: boolean; + condition?: ( + arg: Arg, + { getState }: { getState: AppThunkApi['getState'] }, + ) => boolean; } const createBaseAsyncThunk = createAsyncThunk.withTypes(); @@ -42,7 +46,7 @@ const createBaseAsyncThunk = createAsyncThunk.withTypes(); export function createThunk( name: string, creator: (arg: Arg, api: AppThunkApi) => Returned | Promise, - options: AppThunkOptions = {}, + options: AppThunkOptions = {}, ) { return createBaseAsyncThunk( name, @@ -70,6 +74,7 @@ export function createThunk( if (options.useLoadingBar) return { useLoadingBar: true }; return {}; }, + condition: options.condition, }, ); } @@ -96,7 +101,7 @@ type ArgsType = Record | undefined; export function createDataLoadingThunk( name: string, loadData: (args: Args) => Promise, - thunkOptions?: AppThunkOptions, + thunkOptions?: AppThunkOptions, ): ReturnType>; // Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty @@ -104,17 +109,19 @@ export function createDataLoadingThunk( name: string, loadData: LoadData, onDataOrThunkOptions?: - | AppThunkOptions + | AppThunkOptions | OnData, - thunkOptions?: AppThunkOptions, + thunkOptions?: AppThunkOptions, ): ReturnType>; // Overload when the `onData` method returns nothing, then the mayload is the `onData` result export function createDataLoadingThunk( name: string, loadData: LoadData, - onDataOrThunkOptions?: AppThunkOptions | OnData, - thunkOptions?: AppThunkOptions, + onDataOrThunkOptions?: + | AppThunkOptions + | OnData, + thunkOptions?: AppThunkOptions, ): ReturnType>; // Overload when there is an `onData` method returning something @@ -126,9 +133,9 @@ export function createDataLoadingThunk< name: string, loadData: LoadData, onDataOrThunkOptions?: - | AppThunkOptions + | AppThunkOptions | OnData, - thunkOptions?: AppThunkOptions, + thunkOptions?: AppThunkOptions, ): ReturnType>; /** @@ -154,6 +161,7 @@ export function createDataLoadingThunk< * @param maybeThunkOptions * Additional Mastodon specific options for the thunk. Currently supports: * - `useLoadingBar` to display a loading bar while this action is pending. Defaults to true. + * - `condition` is passed to `createAsyncThunk` (https://redux-toolkit.js.org/api/createAsyncThunk#canceling-before-execution) * @returns The created thunk */ export function createDataLoadingThunk< @@ -164,12 +172,12 @@ export function createDataLoadingThunk< name: string, loadData: LoadData, onDataOrThunkOptions?: - | AppThunkOptions + | AppThunkOptions | OnData, - maybeThunkOptions?: AppThunkOptions, + maybeThunkOptions?: AppThunkOptions, ) { let onData: OnData | undefined; - let thunkOptions: AppThunkOptions | undefined; + let thunkOptions: AppThunkOptions | undefined; if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions; else if (typeof onDataOrThunkOptions === 'object') @@ -203,6 +211,9 @@ export function createDataLoadingThunk< return undefined as Returned; else return result; }, - { useLoadingBar: thunkOptions?.useLoadingBar ?? true }, + { + useLoadingBar: thunkOptions?.useLoadingBar ?? true, + condition: thunkOptions?.condition, + }, ); }