[WiP] Move notification requests actions to their own typed file

cleanup/remove-old-notifications
Claire 2024-09-11 11:55:50 +02:00
parent 920c9d8894
commit da1f8afe1f
8 changed files with 232 additions and 199 deletions

View File

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

View File

@ -0,0 +1,154 @@
import {
apiFetchNotificationRequest,
apiFetchNotificationRequests,
apiFetchNotifications,
} 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';
// 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.getIn(['items'])?.size > 0) {
sinceId = getState().notificationRequests.getIn(['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.from_account)));
return { requests, next: next?.uri };
},
);
export const fetchNotificationRequestsIfNeeded = () =>
async (dispatch: AppDispatch, getState: () => RootState) => {
if (getState().notificationRequests.get('isLoading')) {
return;
}
await dispatch(fetchNotificationRequests());
};
export const fetchNotificationRequest = createDataLoadingThunk(
'notificationRequest/fetch',
async ({ id }: { id: string }) => apiFetchNotificationRequest(id),
);
export const fetchNotificationIfNeeded = (id: string) =>
async (dispatch: AppDispatch, getState: () => RootState) => {
const current = getState().notificationRequests.getIn(['current']);
if (current.getIn(['item', 'id']) === id || current.get('isLoading')) {
return;
}
await dispatch(fetchNotificationRequest({ id }));
}
;
export const expandNotificationRequests = createDataLoadingThunk(
'notificationRequests/expand',
async ({ nextUrl }: { nextUrl: string }) => {
return apiFetchNotificationRequests({}, nextUrl);
},
({ requests, links }, { dispatch }) => {
const next = links.refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(requests.map((request) => request.from_account)));
return { requests, next: next?.uri };
},
);
export const expandNotificationRequestsIfNeeded = () =>
async (dispatch: AppDispatch, getState: () => RootState) => {
const url = getState().notificationRequests.get('next');
if (!url || getState().notificationRequests.get('isLoading')) {
return;
}
await dispatch(expandNotificationRequests(url));
};
export const fetchNotificationsForRequest = createDataLoadingThunk(
'notificationRequest/fetchNotifications',
async ({ accountId, sinceId }: { accountId: string, sinceId?: string }) => {
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 };
},
);
export const fetchNotificationsForRequestIfNeeded = (accountId: string) =>
async (dispatch: AppDispatch, getState: () => RootState) => {
const current = getState().notificationRequests.get('current');
let sinceId = undefined;
if (current.getIn(['item', 'account']) === accountId) {
if (current.getIn(['notifications', 'isLoading'])) {
return;
}
if (current.getIn(['notifications', 'items'])?.size > 0) {
sinceId = current.getIn(['notifications', 'items', 0, 'id']);
}
}
await dispatch(fetchNotificationsForRequest({ accountId, sinceId }));
};

View File

@ -33,18 +33,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';
@ -61,10 +49,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';
@ -217,108 +201,6 @@ export function setBrowserPermission (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));
@ -431,49 +313,6 @@ export const dismissNotificationRequestsFail = (ids, error) => ({
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']);

View File

@ -1,7 +1,28 @@
import api, { apiRequest, getLinks } from 'mastodon/api';
import type { ApiNotificationGroupsResultJSON } from 'mastodon/api_types/notifications';
import api, { apiRequest, getLinks, apiRequestGet } from 'mastodon/api';
import type {
ApiNotificationGroupsResultJSON,
ApiNotificationRequestJSON,
ApiNotificationJSON,
} from 'mastodon/api_types/notifications';
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[];
max_id?: string;
since_id?: string;
@ -24,3 +45,22 @@ export const apiFetchNotifications = async (params?: {
export const apiClearNotifications = () =>
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>(`/api/v1/notifications/requests/${id}`);
};

View File

@ -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;
from_account: ApiAccountJSON;
last_status?: ApiStatusJSON;
}

View File

@ -10,7 +10,8 @@ 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 { fetchNotificationRequestIfNeeded as fetchNotificationRequest } from 'mastodon/actions/notification_requests';
import { fetchNotificationsForRequest, expandNotificationsForRequest, acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
import { IconButton } from 'mastodon/components/icon_button';

View File

@ -11,7 +11,8 @@ 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 { fetchNotificationRequestsIfNeeded as fetchNotificationRequests } from 'mastodon/actions/notification_requests';
import { expandNotificationRequests, acceptNotificationRequests, dismissNotificationRequests } from 'mastodon/actions/notifications';
import { changeSetting } from 'mastodon/actions/settings';
import { CheckBox } from 'mastodon/components/check_box';
import Column from 'mastodon/components/column';

View File

@ -1,23 +1,12 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { blockAccountSuccess, muteAccountSuccess } from 'mastodon/actions/accounts';
import { fetchNotificationRequests, expandNotificationRequests, fetchNotificationRequest, fetchNotificationsForRequest } from 'mastodon/actions/notification_requests';
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,
@ -64,23 +53,23 @@ const removeRequestByAccount = (state, account_id) => {
export const notificationRequestsReducer = (state = initialState, action) => {
switch(action.type) {
case NOTIFICATION_REQUESTS_FETCH_SUCCESS:
case fetchNotificationRequests.success.type:
return state.withMutations(map => {
map.update('items', list => ImmutableList(action.requests.map(normalizeRequest)).concat(list));
map.update('items', list => ImmutableList(action.payload.requests.map(normalizeRequest)).concat(list));
map.set('isLoading', false);
map.update('next', next => next ?? action.next);
map.update('next', next => next ?? action.payload.next);
});
case NOTIFICATION_REQUESTS_EXPAND_SUCCESS:
case expandNotificationRequests.success.type:
return state.withMutations(map => {
map.update('items', list => list.concat(ImmutableList(action.requests.map(normalizeRequest))));
map.update('items', list => list.concat(ImmutableList(action.payload.requests.map(normalizeRequest))));
map.set('isLoading', false);
map.set('next', action.next);
map.set('next', action.payload.next);
});
case NOTIFICATION_REQUESTS_EXPAND_REQUEST:
case NOTIFICATION_REQUESTS_FETCH_REQUEST:
case fetchNotificationRequests.pending.type:
case expandNotificationRequests.pending.type:
return state.set('isLoading', true);
case NOTIFICATION_REQUESTS_EXPAND_FAIL:
case NOTIFICATION_REQUESTS_FETCH_FAIL:
case fetchNotificationRequests.failed.type:
case expandNotificationRequests.failed.type:
return state.set('isLoading', false);
case NOTIFICATION_REQUEST_ACCEPT_REQUEST:
case NOTIFICATION_REQUEST_DISMISS_REQUEST:
@ -92,20 +81,20 @@ export const notificationRequestsReducer = (state = initialState, action) => {
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:
case fetchNotificationRequest.pending.type:
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:
case fetchNotificationRequest.fulfilled.type:
return state.update('current', map => map.set('isLoading', false).set('item', normalizeRequest(action.payload.request)));
case fetchNotificationRequest.failed.type:
return state.update('current', map => map.set('isLoading', false));
case NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST:
case fetchNotificationsForRequest.pending.type:
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 fetchNotificationsForRequest.fulfilled.type:
return state.updateIn(['current', 'notifications'], map => map.set('isLoading', false).update('items', list => ImmutableList(action.payload.notifications.map(notificationToMap)).concat(list)).update('next', next => next ?? action.payload.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 fetchNotificationsForRequest.failed.type:
case NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL:
return state.setIn(['current', 'notifications', 'isLoading'], false);
default: