mirror of https://github.com/mastodon/mastodon.git
Better handle loading notifications from a Gap
parent
f2024050a5
commit
ffad6818cb
|
@ -8,6 +8,7 @@ import {
|
||||||
selectSettingsNotificationsExcludedTypes,
|
selectSettingsNotificationsExcludedTypes,
|
||||||
selectSettingsNotificationsQuickFilterActive,
|
selectSettingsNotificationsQuickFilterActive,
|
||||||
} from 'mastodon/selectors/settings';
|
} from 'mastodon/selectors/settings';
|
||||||
|
import type { AppDispatch } from 'mastodon/store';
|
||||||
import {
|
import {
|
||||||
createAppAsyncThunk,
|
createAppAsyncThunk,
|
||||||
createDataLoadingThunk,
|
createDataLoadingThunk,
|
||||||
|
@ -21,29 +22,13 @@ function excludeAllTypesExcept(filter: string) {
|
||||||
return allNotificationTypes.filter((item) => item !== filter);
|
return allNotificationTypes.filter((item) => item !== filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchNotifications = createDataLoadingThunk(
|
function dispatchAssociatedRecords(
|
||||||
'notificationGroups/fetch',
|
dispatch: AppDispatch,
|
||||||
async (params: { url?: string } | undefined, { getState }) => {
|
notifications: NotificationGroupJSON[],
|
||||||
if (params?.url) return apiFetchNotifications({}, params.url);
|
) {
|
||||||
|
|
||||||
const activeFilter =
|
|
||||||
selectSettingsNotificationsQuickFilterActive(getState());
|
|
||||||
|
|
||||||
return apiFetchNotifications({
|
|
||||||
exclude_types:
|
|
||||||
activeFilter === 'all'
|
|
||||||
? selectSettingsNotificationsExcludedTypes(getState())
|
|
||||||
: excludeAllTypesExcept(activeFilter),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
({ notifications, links }, { dispatch }) => {
|
|
||||||
const fetchedAccounts: ApiAccountJSON[] = [];
|
const fetchedAccounts: ApiAccountJSON[] = [];
|
||||||
const fetchedStatuses: ApiStatusJSON[] = [];
|
const fetchedStatuses: ApiStatusJSON[] = [];
|
||||||
|
|
||||||
// We ignore the previous link, as it will always be here but we know there are no more
|
|
||||||
// recent notifications when doing the initial load
|
|
||||||
const nextLink = links.refs.find((link) => link.rel === 'next');
|
|
||||||
|
|
||||||
notifications.forEach((notification) => {
|
notifications.forEach((notification) => {
|
||||||
if ('sample_accounts' in notification) {
|
if ('sample_accounts' in notification) {
|
||||||
fetchedAccounts.push(...notification.sample_accounts);
|
fetchedAccounts.push(...notification.sample_accounts);
|
||||||
|
@ -67,6 +52,27 @@ export const fetchNotifications = createDataLoadingThunk(
|
||||||
|
|
||||||
if (fetchedStatuses.length > 0)
|
if (fetchedStatuses.length > 0)
|
||||||
dispatch(importFetchedStatuses(fetchedStatuses));
|
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, links }, { dispatch }) => {
|
||||||
|
dispatchAssociatedRecords(dispatch, notifications);
|
||||||
|
|
||||||
|
// We ignore the previous link, as it will always be here but we know there are no more
|
||||||
|
// recent notifications when doing the initial load
|
||||||
|
const nextLink = links.refs.find((link) => link.rel === 'next');
|
||||||
|
|
||||||
const payload: (NotificationGroupJSON | NotificationGap)[] = notifications;
|
const payload: (NotificationGroupJSON | NotificationGap)[] = notifications;
|
||||||
|
|
||||||
|
@ -77,6 +83,20 @@ export const fetchNotifications = createDataLoadingThunk(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const fetchNotificationsGap = createDataLoadingThunk(
|
||||||
|
'notificationGroups/fetchGat',
|
||||||
|
async (params: { gap: NotificationGap }) =>
|
||||||
|
apiFetchNotifications({}, params.gap.loadUrl),
|
||||||
|
|
||||||
|
({ notifications, links }, { dispatch }) => {
|
||||||
|
dispatchAssociatedRecords(dispatch, notifications);
|
||||||
|
|
||||||
|
const nextLink = links.refs.find((link) => link.rel === 'next');
|
||||||
|
|
||||||
|
return { notifications, nextLink };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const setNotificationsFilter = createAppAsyncThunk(
|
export const setNotificationsFilter = createAppAsyncThunk(
|
||||||
'notifications/filter/set',
|
'notifications/filter/set',
|
||||||
({ filterType }: { filterType: string }, { dispatch }) => {
|
({ filterType }: { filterType: string }, { dispatch }) => {
|
||||||
|
|
|
@ -10,11 +10,15 @@ import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
|
import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
|
||||||
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
|
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
|
||||||
import { fetchNotifications } from 'mastodon/actions/notification_groups';
|
import {
|
||||||
|
fetchNotifications,
|
||||||
|
fetchNotificationsGap,
|
||||||
|
} from 'mastodon/actions/notification_groups';
|
||||||
import { compareId } from 'mastodon/compare_id';
|
import { compareId } from 'mastodon/compare_id';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
|
import type { NotificationGap } from 'mastodon/reducers/notifications_groups';
|
||||||
import {
|
import {
|
||||||
selectNeedsNotificationPermission,
|
selectNeedsNotificationPermission,
|
||||||
selectSettingsNotificationsExcludedTypes,
|
selectSettingsNotificationsExcludedTypes,
|
||||||
|
@ -165,9 +169,8 @@ export const Notifications: React.FC<{
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleLoadGap = useCallback(
|
const handleLoadGap = useCallback(
|
||||||
(loadUrl: string) => {
|
(gap: NotificationGap) => {
|
||||||
// TODO: this should not be fetch (as this overrides the existing notifications), but expand?
|
void dispatch(fetchNotificationsGap({ gap }));
|
||||||
void dispatch(fetchNotifications({ url: loadUrl }));
|
|
||||||
},
|
},
|
||||||
[dispatch],
|
[dispatch],
|
||||||
);
|
);
|
||||||
|
@ -269,7 +272,7 @@ export const Notifications: React.FC<{
|
||||||
<LoadGap
|
<LoadGap
|
||||||
key={item.loadUrl}
|
key={item.loadUrl}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
maxId={item.loadUrl}
|
param={item}
|
||||||
onClick={handleLoadGap}
|
onClick={handleLoadGap}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { createReducer } from '@reduxjs/toolkit';
|
import { createReducer, isAnyOf } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import { fetchNotifications } from 'mastodon/actions/notification_groups';
|
import {
|
||||||
|
fetchNotifications,
|
||||||
|
fetchNotificationsGap,
|
||||||
|
} from 'mastodon/actions/notification_groups';
|
||||||
import { createNotificationGroupFromJSON } from 'mastodon/models/notification_group';
|
import { createNotificationGroupFromJSON } from 'mastodon/models/notification_group';
|
||||||
import type { NotificationGroup } from 'mastodon/models/notification_group';
|
import type { NotificationGroup } from 'mastodon/models/notification_group';
|
||||||
|
|
||||||
|
@ -28,19 +31,55 @@ const initialState: NotificationGroupsState = {
|
||||||
export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
||||||
initialState,
|
initialState,
|
||||||
(builder) => {
|
(builder) => {
|
||||||
builder.addCase(fetchNotifications.pending, (state) => {
|
builder
|
||||||
state.isLoading = true;
|
.addCase(fetchNotifications.fulfilled, (state, action) => {
|
||||||
});
|
|
||||||
|
|
||||||
builder.addCase(fetchNotifications.fulfilled, (state, action) => {
|
|
||||||
state.groups = action.payload.map((json) =>
|
state.groups = action.payload.map((json) =>
|
||||||
json.type === 'gap' ? json : createNotificationGroupFromJSON(json),
|
json.type === 'gap' ? json : createNotificationGroupFromJSON(json),
|
||||||
);
|
);
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
});
|
})
|
||||||
|
.addCase(fetchNotificationsGap.fulfilled, (state, action) => {
|
||||||
|
const { notifications, nextLink } = action.payload;
|
||||||
|
|
||||||
|
// find the gap in the existing notifications
|
||||||
|
const gapIndex = state.groups.findIndex(
|
||||||
|
(groupOrGap) =>
|
||||||
|
groupOrGap.type === 'gap' && groupOrGap.loadUrl === nextLink?.uri,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!gapIndex)
|
||||||
|
// We do not know where to insert, let's return
|
||||||
|
return;
|
||||||
|
|
||||||
|
// replace the gap with the notifications + a new gap
|
||||||
|
|
||||||
|
const toInsert: NotificationGroupsState['groups'] = notifications.map(
|
||||||
|
(json) => createNotificationGroupFromJSON(json),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextLink?.uri && notifications.length > 0) {
|
||||||
|
// If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap
|
||||||
|
toInsert.push({
|
||||||
|
type: 'gap',
|
||||||
|
loadUrl: nextLink.uri,
|
||||||
|
} as NotificationGap);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.groups.splice(gapIndex, 1, ...toInsert);
|
||||||
|
|
||||||
builder.addCase(fetchNotifications.rejected, (state) => {
|
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
});
|
})
|
||||||
|
.addMatcher(
|
||||||
|
isAnyOf(fetchNotifications.pending, fetchNotificationsGap.pending),
|
||||||
|
(state) => {
|
||||||
|
state.isLoading = true;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.addMatcher(
|
||||||
|
isAnyOf(fetchNotifications.rejected, fetchNotificationsGap.rejected),
|
||||||
|
(state) => {
|
||||||
|
state.isLoading = false;
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue