Grouped Notifications UI (#30440)
Co-authored-by: Eugen Rochko <eugen@zeonfederated.com> Co-authored-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
7d090b2ab6
commit
f587ff643f
65 changed files with 3329 additions and 131 deletions
|
|
@ -75,9 +75,17 @@ interface MarkerParam {
|
|||
}
|
||||
|
||||
function getLastNotificationId(state: RootState): string | undefined {
|
||||
// @ts-expect-error state.notifications is not yet typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
|
||||
return state.getIn(['notifications', 'lastReadId']);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const enableBeta = state.settings.getIn(
|
||||
['notifications', 'groupingBeta'],
|
||||
false,
|
||||
) as boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return enableBeta
|
||||
? state.notificationGroups.lastReadId
|
||||
: // @ts-expect-error state.notifications is not yet typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
state.getIn(['notifications', 'lastReadId']);
|
||||
}
|
||||
|
||||
const buildPostMarkersParams = (state: RootState) => {
|
||||
|
|
|
|||
144
app/javascript/mastodon/actions/notification_groups.ts
Normal file
144
app/javascript/mastodon/actions/notification_groups.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import {
|
||||
apiClearNotifications,
|
||||
apiFetchNotifications,
|
||||
} from 'mastodon/api/notifications';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import type {
|
||||
ApiNotificationGroupJSON,
|
||||
ApiNotificationJSON,
|
||||
} from 'mastodon/api_types/notifications';
|
||||
import { allNotificationTypes } from 'mastodon/api_types/notifications';
|
||||
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
||||
import type { NotificationGap } from 'mastodon/reducers/notification_groups';
|
||||
import {
|
||||
selectSettingsNotificationsExcludedTypes,
|
||||
selectSettingsNotificationsQuickFilterActive,
|
||||
} from 'mastodon/selectors/settings';
|
||||
import type { AppDispatch } from 'mastodon/store';
|
||||
import {
|
||||
createAppAsyncThunk,
|
||||
createDataLoadingThunk,
|
||||
} from 'mastodon/store/typed_functions';
|
||||
|
||||
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||
import { NOTIFICATIONS_FILTER_SET } from './notifications';
|
||||
import { saveSettings } from './settings';
|
||||
|
||||
function excludeAllTypesExcept(filter: string) {
|
||||
return allNotificationTypes.filter((item) => item !== filter);
|
||||
}
|
||||
|
||||
function dispatchAssociatedRecords(
|
||||
dispatch: AppDispatch,
|
||||
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
|
||||
) {
|
||||
const fetchedAccounts: ApiAccountJSON[] = [];
|
||||
const fetchedStatuses: ApiStatusJSON[] = [];
|
||||
|
||||
notifications.forEach((notification) => {
|
||||
if ('sample_accounts' in notification) {
|
||||
fetchedAccounts.push(...notification.sample_accounts);
|
||||
}
|
||||
|
||||
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) {
|
||||
fetchedStatuses.push(notification.status);
|
||||
}
|
||||
});
|
||||
|
||||
if (fetchedAccounts.length > 0)
|
||||
dispatch(importFetchedAccounts(fetchedAccounts));
|
||||
|
||||
if (fetchedStatuses.length > 0)
|
||||
dispatch(importFetchedStatuses(fetchedStatuses));
|
||||
}
|
||||
|
||||
export const fetchNotifications = createDataLoadingThunk(
|
||||
'notificationGroups/fetch',
|
||||
async (_params, { getState }) => {
|
||||
const activeFilter =
|
||||
selectSettingsNotificationsQuickFilterActive(getState());
|
||||
|
||||
return apiFetchNotifications({
|
||||
exclude_types:
|
||||
activeFilter === 'all'
|
||||
? selectSettingsNotificationsExcludedTypes(getState())
|
||||
: excludeAllTypesExcept(activeFilter),
|
||||
});
|
||||
},
|
||||
({ notifications }, { dispatch }) => {
|
||||
dispatchAssociatedRecords(dispatch, notifications);
|
||||
const payload: (ApiNotificationGroupJSON | NotificationGap)[] =
|
||||
notifications;
|
||||
|
||||
// TODO: might be worth not using gaps for that…
|
||||
// if (nextLink) payload.push({ type: 'gap', loadUrl: nextLink.uri });
|
||||
if (notifications.length > 1)
|
||||
payload.push({ type: 'gap', maxId: notifications.at(-1)?.page_min_id });
|
||||
|
||||
return payload;
|
||||
// dispatch(submitMarkers());
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchNotificationsGap = createDataLoadingThunk(
|
||||
'notificationGroups/fetchGap',
|
||||
async (params: { gap: NotificationGap }) =>
|
||||
apiFetchNotifications({ max_id: params.gap.maxId }),
|
||||
|
||||
({ notifications }, { dispatch }) => {
|
||||
dispatchAssociatedRecords(dispatch, notifications);
|
||||
|
||||
return { notifications };
|
||||
},
|
||||
);
|
||||
|
||||
export const processNewNotificationForGroups = createAppAsyncThunk(
|
||||
'notificationGroups/processNew',
|
||||
(notification: ApiNotificationJSON, { dispatch }) => {
|
||||
dispatchAssociatedRecords(dispatch, [notification]);
|
||||
|
||||
return notification;
|
||||
},
|
||||
);
|
||||
|
||||
export const loadPending = createAction('notificationGroups/loadPending');
|
||||
|
||||
export const updateScrollPosition = createAction<{ top: boolean }>(
|
||||
'notificationGroups/updateScrollPosition',
|
||||
);
|
||||
|
||||
export const setNotificationsFilter = createAppAsyncThunk(
|
||||
'notifications/filter/set',
|
||||
({ filterType }: { filterType: string }, { dispatch }) => {
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_FILTER_SET,
|
||||
path: ['notifications', 'quickFilter', 'active'],
|
||||
value: filterType,
|
||||
});
|
||||
// dispatch(expandNotifications({ forceLoad: true }));
|
||||
void dispatch(fetchNotifications());
|
||||
dispatch(saveSettings());
|
||||
},
|
||||
);
|
||||
|
||||
export const clearNotifications = createDataLoadingThunk(
|
||||
'notifications/clear',
|
||||
() => apiClearNotifications(),
|
||||
);
|
||||
|
||||
export const markNotificationsAsRead = createAction(
|
||||
'notificationGroups/markAsRead',
|
||||
);
|
||||
|
||||
export const mountNotifications = createAction('notificationGroups/mount');
|
||||
export const unmountNotifications = createAction('notificationGroups/unmount');
|
||||
|
|
@ -32,7 +32,6 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
|
|||
|
||||
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
|
||||
|
||||
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
||||
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
|
||||
|
||||
|
|
@ -174,7 +173,7 @@ const noOp = () => {};
|
|||
|
||||
let expandNotificationsController = new AbortController();
|
||||
|
||||
export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
|
||||
export function expandNotifications({ maxId, forceLoad = false } = {}, done = noOp) {
|
||||
return (dispatch, getState) => {
|
||||
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
|
||||
const notifications = getState().get('notifications');
|
||||
|
|
@ -257,16 +256,6 @@ export function expandNotificationsFail(error, isLoadingMore) {
|
|||
};
|
||||
}
|
||||
|
||||
export function clearNotifications() {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: NOTIFICATIONS_CLEAR,
|
||||
});
|
||||
|
||||
api().post('/api/v1/notifications/clear');
|
||||
};
|
||||
}
|
||||
|
||||
export function scrollTopNotifications(top) {
|
||||
return {
|
||||
type: NOTIFICATIONS_SCROLL_TOP,
|
||||
|
|
|
|||
18
app/javascript/mastodon/actions/notifications_migration.tsx
Normal file
18
app/javascript/mastodon/actions/notifications_migration.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { createAppAsyncThunk } from 'mastodon/store';
|
||||
|
||||
import { fetchNotifications } from './notification_groups';
|
||||
import { expandNotifications } from './notifications';
|
||||
|
||||
export const initializeNotifications = createAppAsyncThunk(
|
||||
'notifications/initialize',
|
||||
(_, { dispatch, getState }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const enableBeta = getState().settings.getIn(
|
||||
['notifications', 'groupingBeta'],
|
||||
false,
|
||||
) as boolean;
|
||||
|
||||
if (enableBeta) void dispatch(fetchNotifications());
|
||||
else dispatch(expandNotifications());
|
||||
},
|
||||
);
|
||||
|
|
@ -1,11 +1,6 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import type { ApiAccountJSON } from '../api_types/accounts';
|
||||
// To be replaced once ApiNotificationJSON type exists
|
||||
interface FakeApiNotificationJSON {
|
||||
type: string;
|
||||
account: ApiAccountJSON;
|
||||
}
|
||||
import type { ApiNotificationJSON } from 'mastodon/api_types/notifications';
|
||||
|
||||
export const notificationsUpdate = createAction(
|
||||
'notifications/update',
|
||||
|
|
@ -13,7 +8,7 @@ export const notificationsUpdate = createAction(
|
|||
playSound,
|
||||
...args
|
||||
}: {
|
||||
notification: FakeApiNotificationJSON;
|
||||
notification: ApiNotificationJSON;
|
||||
usePendingItems: boolean;
|
||||
playSound: boolean;
|
||||
}) => ({
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
deleteAnnouncement,
|
||||
} from './announcements';
|
||||
import { updateConversations } from './conversations';
|
||||
import { processNewNotificationForGroups } from './notification_groups';
|
||||
import { updateNotifications, expandNotifications } from './notifications';
|
||||
import { updateStatus } from './statuses';
|
||||
import {
|
||||
|
|
@ -98,10 +99,16 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
case 'delete':
|
||||
dispatch(deleteFromTimelines(data.payload));
|
||||
break;
|
||||
case 'notification':
|
||||
case 'notification': {
|
||||
// @ts-expect-error
|
||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
||||
const notificationJSON = JSON.parse(data.payload);
|
||||
dispatch(updateNotifications(notificationJSON, messages, locale));
|
||||
// TODO: remove this once the groups feature replaces the previous one
|
||||
if(getState().notificationGroups.groups.length > 0) {
|
||||
dispatch(processNewNotificationForGroups(notificationJSON));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'conversation':
|
||||
// @ts-expect-error
|
||||
dispatch(updateConversations(JSON.parse(data.payload)));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue