Convert notification requests actions and reducers to Typescript (#31866)

pull/2846/head
Claire 2024-09-16 11:54:03 +02:00 committed by GitHub
parent d5cf27e667
commit c0eda832f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 585 additions and 492 deletions

View File

@ -2,7 +2,7 @@ import { createAction } from '@reduxjs/toolkit';
import { import {
apiClearNotifications, apiClearNotifications,
apiFetchNotifications, apiFetchNotificationGroups,
} from 'mastodon/api/notifications'; } from 'mastodon/api/notifications';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type { import type {
@ -71,7 +71,7 @@ function dispatchAssociatedRecords(
export const fetchNotifications = createDataLoadingThunk( export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch', 'notificationGroups/fetch',
async (_params, { getState }) => async (_params, { getState }) =>
apiFetchNotifications({ exclude_types: getExcludedTypes(getState()) }), apiFetchNotificationGroups({ exclude_types: getExcludedTypes(getState()) }),
({ notifications, accounts, statuses }, { dispatch }) => { ({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts)); dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses)); dispatch(importFetchedStatuses(statuses));
@ -92,7 +92,7 @@ export const fetchNotifications = createDataLoadingThunk(
export const fetchNotificationsGap = createDataLoadingThunk( export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap', 'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }, { getState }) => async (params: { gap: NotificationGap }, { getState }) =>
apiFetchNotifications({ apiFetchNotificationGroups({
max_id: params.gap.maxId, max_id: params.gap.maxId,
exclude_types: getExcludedTypes(getState()), exclude_types: getExcludedTypes(getState()),
}), }),
@ -108,7 +108,7 @@ export const fetchNotificationsGap = createDataLoadingThunk(
export const pollRecentNotifications = createDataLoadingThunk( export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications', 'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => { async (_params, { getState }) => {
return apiFetchNotifications({ return apiFetchNotificationGroups({
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

View File

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

View File

@ -18,7 +18,6 @@ import {
importFetchedStatuses, importFetchedStatuses,
} from './importer'; } from './importer';
import { submitMarkers } from './markers'; import { submitMarkers } from './markers';
import { decreasePendingNotificationsCount } from './notification_policies';
import { notificationsUpdate } from "./notifications_typed"; import { notificationsUpdate } from "./notifications_typed";
import { register as registerPushNotifications } from './push_notifications'; import { register as registerPushNotifications } from './push_notifications';
import { saveSettings } from './settings'; 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_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; 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_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST';
export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS'; export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS';
export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL'; 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_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS';
export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL'; 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({ defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, 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 = () => ({ export const loadPending = () => ({
type: NOTIFICATIONS_LOAD_PENDING, type: NOTIFICATIONS_LOAD_PENDING,
}); });
@ -343,296 +308,3 @@ export function setBrowserPermission (value) {
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,
});

View File

@ -1,7 +1,36 @@
import api, { apiRequest, getLinks } from 'mastodon/api'; import api, {
import type { ApiNotificationGroupsResultJSON } from 'mastodon/api_types/notifications'; 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<ApiNotificationJSON[]>({
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[]; exclude_types?: string[];
max_id?: string; max_id?: string;
since_id?: string; since_id?: string;
@ -24,3 +53,43 @@ export const apiFetchNotifications = async (params?: {
export const apiClearNotifications = () => export const apiClearNotifications = () =>
apiRequest<undefined>('POST', 'v1/notifications/clear'); apiRequest<undefined>('POST', 'v1/notifications/clear');
export const apiFetchNotificationRequests = async (
params?: {
since_id?: string;
},
url?: string,
) => {
const response = await api().request<ApiNotificationRequestJSON[]>({
method: 'GET',
url: url ?? '/api/v1/notifications/requests',
params,
});
return {
requests: response.data,
links: getLinks(response),
};
};
export const apiFetchNotificationRequest = async (id: string) => {
return apiRequestGet<ApiNotificationRequestJSON>(
`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 });
};

View File

@ -149,3 +149,12 @@ export interface ApiNotificationGroupsResultJSON {
statuses: ApiStatusJSON[]; statuses: ApiStatusJSON[];
notification_groups: ApiNotificationGroupJSON[]; notification_groups: ApiNotificationGroupJSON[];
} }
export interface ApiNotificationRequestJSON {
id: string;
created_at: string;
updated_at: string;
notifications_count: string;
account: ApiAccountJSON;
last_status?: ApiStatusJSON;
}

View File

@ -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 MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { initBlockModal } from 'mastodon/actions/blocks'; import { initBlockModal } from 'mastodon/actions/blocks';
import { initMuteModal } from 'mastodon/actions/mutes'; 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 { initReport } from 'mastodon/actions/reports';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import { CheckBox } from 'mastodon/components/check_box'; import { CheckBox } from 'mastodon/components/check_box';
@ -40,11 +40,11 @@ export const NotificationRequest = ({ id, accountId, notificationsCount, checked
const { push: historyPush } = useHistory(); const { push: historyPush } = useHistory();
const handleDismiss = useCallback(() => { const handleDismiss = useCallback(() => {
dispatch(dismissNotificationRequest(id)); dispatch(dismissNotificationRequest({ id }));
}, [dispatch, id]); }, [dispatch, id]);
const handleAccept = useCallback(() => { const handleAccept = useCallback(() => {
dispatch(acceptNotificationRequest(id)); dispatch(acceptNotificationRequest({ id }));
}, [dispatch, id]); }, [dispatch, id]);
const handleMute = useCallback(() => { const handleMute = useCallback(() => {

View File

@ -10,7 +10,13 @@ import { useSelector, useDispatch } from 'react-redux';
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react'; import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import DoneIcon from '@/material-icons/400-24px/done.svg?react'; import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import InventoryIcon from '@/material-icons/400-24px/inventory_2.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 Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
@ -44,28 +50,28 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
const columnRef = useRef(); const columnRef = useRef();
const intl = useIntl(); const intl = useIntl();
const dispatch = useDispatch(); const dispatch = useDispatch();
const notificationRequest = useSelector(state => state.getIn(['notificationRequests', 'current', 'item', 'id']) === id ? state.getIn(['notificationRequests', 'current', 'item']) : null); const notificationRequest = useSelector(state => state.notificationRequests.current.item?.id === id ? state.notificationRequests.current.item : null);
const accountId = notificationRequest?.get('account'); const accountId = notificationRequest?.account_id;
const account = useSelector(state => state.getIn(['accounts', accountId])); const account = useSelector(state => state.getIn(['accounts', accountId]));
const notifications = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'items'])); const notifications = useSelector(state => state.notificationRequests.current.notifications.items);
const isLoading = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'isLoading'])); const isLoading = useSelector(state => state.notificationRequests.current.notifications.isLoading);
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'current', 'notifications', 'next'])); const hasMore = useSelector(state => !!state.notificationRequests.current.notifications.next);
const removed = useSelector(state => state.getIn(['notificationRequests', 'current', 'removed'])); const removed = useSelector(state => state.notificationRequests.current.removed);
const handleHeaderClick = useCallback(() => { const handleHeaderClick = useCallback(() => {
columnRef.current?.scrollTop(); columnRef.current?.scrollTop();
}, [columnRef]); }, [columnRef]);
const handleLoadMore = useCallback(() => { const handleLoadMore = useCallback(() => {
dispatch(expandNotificationsForRequest()); dispatch(expandNotificationsForRequest({ accountId }));
}, [dispatch]); }, [dispatch, accountId]);
const handleDismiss = useCallback(() => { const handleDismiss = useCallback(() => {
dispatch(dismissNotificationRequest(id)); dispatch(dismissNotificationRequest({ id }));
}, [dispatch, id]); }, [dispatch, id]);
const handleAccept = useCallback(() => { const handleAccept = useCallback(() => {
dispatch(acceptNotificationRequest(id)); dispatch(acceptNotificationRequest({ id }));
}, [dispatch, id]); }, [dispatch, id]);
const handleMoveUp = useCallback(id => { const handleMoveUp = useCallback(id => {
@ -79,12 +85,12 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
}, [columnRef, notifications]); }, [columnRef, notifications]);
useEffect(() => { useEffect(() => {
dispatch(fetchNotificationRequest(id)); dispatch(fetchNotificationRequest({ id }));
}, [dispatch, id]); }, [dispatch, id]);
useEffect(() => { useEffect(() => {
if (accountId) { if (accountId) {
dispatch(fetchNotificationsForRequest(accountId)); dispatch(fetchNotificationsForRequest({ accountId }));
} }
}, [dispatch, accountId]); }, [dispatch, accountId]);

View File

@ -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 InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { openModal } from 'mastodon/actions/modal'; 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 { changeSetting } from 'mastodon/actions/settings';
import { CheckBox } from 'mastodon/components/check_box'; import { CheckBox } from 'mastodon/components/check_box';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
@ -84,7 +89,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
message: intl.formatMessage(messages.confirmAcceptMultipleMessage, { count: selectedItems.length }), message: intl.formatMessage(messages.confirmAcceptMultipleMessage, { count: selectedItems.length }),
confirm: intl.formatMessage(messages.confirmAcceptMultipleButton, { count: selectedItems.length}), confirm: intl.formatMessage(messages.confirmAcceptMultipleButton, { count: selectedItems.length}),
onConfirm: () => onConfirm: () =>
dispatch(acceptNotificationRequests(selectedItems)), dispatch(acceptNotificationRequests({ ids: selectedItems })),
}, },
})); }));
}, [dispatch, intl, selectedItems]); }, [dispatch, intl, selectedItems]);
@ -97,7 +102,7 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
message: intl.formatMessage(messages.confirmDismissMultipleMessage, { count: selectedItems.length }), message: intl.formatMessage(messages.confirmDismissMultipleMessage, { count: selectedItems.length }),
confirm: intl.formatMessage(messages.confirmDismissMultipleButton, { count: selectedItems.length}), confirm: intl.formatMessage(messages.confirmDismissMultipleButton, { count: selectedItems.length}),
onConfirm: () => onConfirm: () =>
dispatch(dismissNotificationRequests(selectedItems)), dispatch(dismissNotificationRequests({ ids: selectedItems })),
}, },
})); }));
}, [dispatch, intl, selectedItems]); }, [dispatch, intl, selectedItems]);
@ -161,9 +166,9 @@ export const NotificationRequests = ({ multiColumn }) => {
const columnRef = useRef(); const columnRef = useRef();
const intl = useIntl(); const intl = useIntl();
const dispatch = useDispatch(); const dispatch = useDispatch();
const isLoading = useSelector(state => state.getIn(['notificationRequests', 'isLoading'])); const isLoading = useSelector(state => state.notificationRequests.isLoading);
const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items'])); const notificationRequests = useSelector(state => state.notificationRequests.items);
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'next'])); const hasMore = useSelector(state => !!state.notificationRequests.next);
const [selectionMode, setSelectionMode] = useState(false); const [selectionMode, setSelectionMode] = useState(false);
const [checkedRequestIds, setCheckedRequestIds] = useState([]); const [checkedRequestIds, setCheckedRequestIds] = useState([]);
@ -182,7 +187,7 @@ export const NotificationRequests = ({ multiColumn }) => {
else else
ids.push(id); ids.push(id);
setSelectAllChecked(ids.length === notificationRequests.size); setSelectAllChecked(ids.length === notificationRequests.length);
return [...ids]; return [...ids];
}); });
@ -193,7 +198,7 @@ export const NotificationRequests = ({ multiColumn }) => {
if(checked) if(checked)
setCheckedRequestIds([]); setCheckedRequestIds([]);
else else
setCheckedRequestIds(notificationRequests.map(request => request.get('id')).toArray()); setCheckedRequestIds(notificationRequests.map(request => request.id));
return !checked; return !checked;
}); });
@ -217,7 +222,7 @@ export const NotificationRequests = ({ multiColumn }) => {
multiColumn={multiColumn} multiColumn={multiColumn}
showBackButton showBackButton
appendContent={ appendContent={
notificationRequests.size > 0 && ( notificationRequests.length > 0 && (
<SelectRow selectionMode={selectionMode} setSelectionMode={setSelectionMode} selectAllChecked={selectAllChecked} toggleSelectAll={toggleSelectAll} selectedItems={checkedRequestIds} /> <SelectRow selectionMode={selectionMode} setSelectionMode={setSelectionMode} selectAllChecked={selectAllChecked} toggleSelectAll={toggleSelectAll} selectedItems={checkedRequestIds} />
)} )}
> >
@ -236,12 +241,12 @@ export const NotificationRequests = ({ multiColumn }) => {
> >
{notificationRequests.map(request => ( {notificationRequests.map(request => (
<NotificationRequest <NotificationRequest
key={request.get('id')} key={request.id}
id={request.get('id')} id={request.id}
accountId={request.get('account')} accountId={request.account_id}
notificationsCount={request.get('notifications_count')} notificationsCount={request.notifications_count}
showCheckbox={selectionMode} showCheckbox={selectionMode}
checked={checkedRequestIds.includes(request.get('id'))} checked={checkedRequestIds.includes(request.id)}
toggleCheck={handleCheck} toggleCheck={handleCheck}
/> />
))} ))}

View File

@ -0,0 +1,19 @@
import type { ApiNotificationRequestJSON } from 'mastodon/api_types/notifications';
export interface NotificationRequest
extends Omit<ApiNotificationRequestJSON, 'account' | 'notifications_count'> {
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,
};
}

View File

@ -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;
}
};

View File

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

View File

@ -33,8 +33,12 @@ interface AppThunkConfig {
} }
type AppThunkApi = Pick<GetThunkAPI<AppThunkConfig>, 'getState' | 'dispatch'>; type AppThunkApi = Pick<GetThunkAPI<AppThunkConfig>, 'getState' | 'dispatch'>;
interface AppThunkOptions { interface AppThunkOptions<Arg> {
useLoadingBar?: boolean; useLoadingBar?: boolean;
condition?: (
arg: Arg,
{ getState }: { getState: AppThunkApi['getState'] },
) => boolean;
} }
const createBaseAsyncThunk = createAsyncThunk.withTypes<AppThunkConfig>(); const createBaseAsyncThunk = createAsyncThunk.withTypes<AppThunkConfig>();
@ -42,7 +46,7 @@ const createBaseAsyncThunk = createAsyncThunk.withTypes<AppThunkConfig>();
export function createThunk<Arg = void, Returned = void>( export function createThunk<Arg = void, Returned = void>(
name: string, name: string,
creator: (arg: Arg, api: AppThunkApi) => Returned | Promise<Returned>, creator: (arg: Arg, api: AppThunkApi) => Returned | Promise<Returned>,
options: AppThunkOptions = {}, options: AppThunkOptions<Arg> = {},
) { ) {
return createBaseAsyncThunk( return createBaseAsyncThunk(
name, name,
@ -70,6 +74,7 @@ export function createThunk<Arg = void, Returned = void>(
if (options.useLoadingBar) return { useLoadingBar: true }; if (options.useLoadingBar) return { useLoadingBar: true };
return {}; return {};
}, },
condition: options.condition,
}, },
); );
} }
@ -96,7 +101,7 @@ type ArgsType = Record<string, unknown> | undefined;
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>( export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string, name: string,
loadData: (args: Args) => Promise<LoadDataResult>, loadData: (args: Args) => Promise<LoadDataResult>,
thunkOptions?: AppThunkOptions, thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, LoadDataResult>>; ): ReturnType<typeof createThunk<Args, LoadDataResult>>;
// Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty // Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty
@ -104,17 +109,19 @@ export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string, name: string,
loadData: LoadData<Args, LoadDataResult>, loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: onDataOrThunkOptions?:
| AppThunkOptions | AppThunkOptions<Args>
| OnData<Args, LoadDataResult, DiscardLoadData>, | OnData<Args, LoadDataResult, DiscardLoadData>,
thunkOptions?: AppThunkOptions, thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, void>>; ): ReturnType<typeof createThunk<Args, void>>;
// Overload when the `onData` method returns nothing, then the mayload is the `onData` result // Overload when the `onData` method returns nothing, then the mayload is the `onData` result
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>( export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string, name: string,
loadData: LoadData<Args, LoadDataResult>, loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: AppThunkOptions | OnData<Args, LoadDataResult, void>, onDataOrThunkOptions?:
thunkOptions?: AppThunkOptions, | AppThunkOptions<Args>
| OnData<Args, LoadDataResult, void>,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, LoadDataResult>>; ): ReturnType<typeof createThunk<Args, LoadDataResult>>;
// Overload when there is an `onData` method returning something // Overload when there is an `onData` method returning something
@ -126,9 +133,9 @@ export function createDataLoadingThunk<
name: string, name: string,
loadData: LoadData<Args, LoadDataResult>, loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: onDataOrThunkOptions?:
| AppThunkOptions | AppThunkOptions<Args>
| OnData<Args, LoadDataResult, Returned>, | OnData<Args, LoadDataResult, Returned>,
thunkOptions?: AppThunkOptions, thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, Returned>>; ): ReturnType<typeof createThunk<Args, Returned>>;
/** /**
@ -154,6 +161,7 @@ export function createDataLoadingThunk<
* @param maybeThunkOptions * @param maybeThunkOptions
* Additional Mastodon specific options for the thunk. Currently supports: * Additional Mastodon specific options for the thunk. Currently supports:
* - `useLoadingBar` to display a loading bar while this action is pending. Defaults to true. * - `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 * @returns The created thunk
*/ */
export function createDataLoadingThunk< export function createDataLoadingThunk<
@ -164,12 +172,12 @@ export function createDataLoadingThunk<
name: string, name: string,
loadData: LoadData<Args, LoadDataResult>, loadData: LoadData<Args, LoadDataResult>,
onDataOrThunkOptions?: onDataOrThunkOptions?:
| AppThunkOptions | AppThunkOptions<Args>
| OnData<Args, LoadDataResult, Returned>, | OnData<Args, LoadDataResult, Returned>,
maybeThunkOptions?: AppThunkOptions, maybeThunkOptions?: AppThunkOptions<Args>,
) { ) {
let onData: OnData<Args, LoadDataResult, Returned> | undefined; let onData: OnData<Args, LoadDataResult, Returned> | undefined;
let thunkOptions: AppThunkOptions | undefined; let thunkOptions: AppThunkOptions<Args> | undefined;
if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions; if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions;
else if (typeof onDataOrThunkOptions === 'object') else if (typeof onDataOrThunkOptions === 'object')
@ -203,6 +211,9 @@ export function createDataLoadingThunk<
return undefined as Returned; return undefined as Returned;
else return result; else return result;
}, },
{ useLoadingBar: thunkOptions?.useLoadingBar ?? true }, {
useLoadingBar: thunkOptions?.useLoadingBar ?? true,
condition: thunkOptions?.condition,
},
); );
} }