mirror of https://github.com/mastodon/mastodon
Convert notification requests actions and reducers to Typescript (#31866)
parent
d5cf27e667
commit
c0eda832f3
@ -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;
|
||||
},
|
||||
);
|
@ -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,
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
};
|
@ -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;
|
||||
},
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue