mirror of https://github.com/mastodon/mastodon.git
[WiP] Improve gap handling
parent
5b73eb5f8e
commit
6068118643
|
@ -73,17 +73,15 @@ export const fetchNotifications = createDataLoadingThunk(
|
||||||
: excludeAllTypesExcept(activeFilter),
|
: excludeAllTypesExcept(activeFilter),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
({ notifications, links }, { dispatch }) => {
|
({ notifications }, { dispatch }) => {
|
||||||
dispatchAssociatedRecords(dispatch, notifications);
|
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: (ApiNotificationGroupJSON | NotificationGap)[] =
|
const payload: (ApiNotificationGroupJSON | NotificationGap)[] =
|
||||||
notifications;
|
notifications;
|
||||||
|
|
||||||
if (nextLink) payload.push({ type: 'gap', loadUrl: nextLink.uri });
|
// 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;
|
return payload;
|
||||||
// dispatch(submitMarkers());
|
// dispatch(submitMarkers());
|
||||||
|
@ -93,14 +91,12 @@ export const fetchNotifications = createDataLoadingThunk(
|
||||||
export const fetchNotificationsGap = createDataLoadingThunk(
|
export const fetchNotificationsGap = createDataLoadingThunk(
|
||||||
'notificationGroups/fetchGat',
|
'notificationGroups/fetchGat',
|
||||||
async (params: { gap: NotificationGap }) =>
|
async (params: { gap: NotificationGap }) =>
|
||||||
apiFetchNotifications({}, params.gap.loadUrl),
|
apiFetchNotifications({ max_id: params.gap.maxId }),
|
||||||
|
|
||||||
({ notifications, links }, { dispatch }) => {
|
({ notifications }, { dispatch }) => {
|
||||||
dispatchAssociatedRecords(dispatch, notifications);
|
dispatchAssociatedRecords(dispatch, notifications);
|
||||||
|
|
||||||
const nextLink = links.refs.find((link) => link.rel === 'next');
|
return { notifications };
|
||||||
|
|
||||||
return { notifications, nextLink };
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import api, { apiRequest, getLinks } from 'mastodon/api';
|
import api, { apiRequest, getLinks } from 'mastodon/api';
|
||||||
import type { ApiNotificationGroupJSON } from 'mastodon/api_types/notifications';
|
import type { ApiNotificationGroupJSON } from 'mastodon/api_types/notifications';
|
||||||
|
|
||||||
export const apiFetchNotifications = async (
|
export const apiFetchNotifications = async (params?: {
|
||||||
params?: {
|
|
||||||
exclude_types?: string[];
|
exclude_types?: string[];
|
||||||
},
|
max_id?: string;
|
||||||
forceUrl?: string,
|
}) => {
|
||||||
) => {
|
|
||||||
const response = await api().request<ApiNotificationGroupJSON[]>({
|
const response = await api().request<ApiNotificationGroupJSON[]>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: forceUrl ?? '/api/v2_alpha/notifications',
|
url: '/api/v2_alpha/notifications',
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -254,7 +254,7 @@ export const Notifications: React.FC<{
|
||||||
return notifications.map((item) =>
|
return notifications.map((item) =>
|
||||||
item.type === 'gap' ? (
|
item.type === 'gap' ? (
|
||||||
<LoadGap
|
<LoadGap
|
||||||
key={item.loadUrl}
|
key={`${item.maxId}-${item.sinceId}`}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
param={item}
|
param={item}
|
||||||
onClick={handleLoadGap}
|
onClick={handleLoadGap}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
timelineDelete,
|
timelineDelete,
|
||||||
} from 'mastodon/actions/timelines_typed';
|
} from 'mastodon/actions/timelines_typed';
|
||||||
|
import { compareId } from 'mastodon/compare_id';
|
||||||
import {
|
import {
|
||||||
NOTIFICATIONS_GROUP_MAX_AVATARS,
|
NOTIFICATIONS_GROUP_MAX_AVATARS,
|
||||||
createNotificationGroupFromJSON,
|
createNotificationGroupFromJSON,
|
||||||
|
@ -26,7 +27,8 @@ import type { NotificationGroup } from 'mastodon/models/notification_group';
|
||||||
|
|
||||||
export interface NotificationGap {
|
export interface NotificationGap {
|
||||||
type: 'gap';
|
type: 'gap';
|
||||||
loadUrl: string;
|
maxId?: string;
|
||||||
|
sinceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationGroupsState {
|
interface NotificationGroupsState {
|
||||||
|
@ -93,32 +95,67 @@ export const notificationsGroupsReducer =
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
})
|
})
|
||||||
.addCase(fetchNotificationsGap.fulfilled, (state, action) => {
|
.addCase(fetchNotificationsGap.fulfilled, (state, action) => {
|
||||||
const { notifications, nextLink } = action.payload;
|
const { notifications } = action.payload;
|
||||||
|
|
||||||
// find the gap in the existing notifications
|
// find the gap in the existing notifications
|
||||||
const gapIndex = state.groups.findIndex(
|
const gapIndex = state.groups.findIndex(
|
||||||
(groupOrGap) =>
|
(groupOrGap) =>
|
||||||
groupOrGap.type === 'gap' && groupOrGap.loadUrl === nextLink?.uri,
|
groupOrGap.type === 'gap' &&
|
||||||
|
groupOrGap.sinceId === action.meta.arg.gap.sinceId &&
|
||||||
|
groupOrGap.maxId === action.meta.arg.gap.maxId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!gapIndex)
|
if (gapIndex < 0)
|
||||||
// We do not know where to insert, let's return
|
// We do not know where to insert, let's return
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Filling a disconnection gap means we're getting historical data
|
||||||
|
// about groups we may know or may not know about.
|
||||||
|
|
||||||
|
// The notifications timeline is split in two by the gap, with
|
||||||
|
// group information newer than the gap, and group information older
|
||||||
|
// than the gap.
|
||||||
|
|
||||||
|
// Filling a gap should not touch anything before the gap, so any
|
||||||
|
// information on groups already appearing before the gap should be
|
||||||
|
// discarded, while any information on groups appearing after the gap
|
||||||
|
// can be updated and re-ordered.
|
||||||
|
|
||||||
|
const oldestPageNotification = notifications.at(-1)?.page_min_id;
|
||||||
|
|
||||||
// replace the gap with the notifications + a new gap
|
// replace the gap with the notifications + a new gap
|
||||||
|
|
||||||
const toInsert: NotificationGroupsState['groups'] = notifications.map(
|
const toInsert: NotificationGroupsState['groups'] = notifications
|
||||||
(json) => createNotificationGroupFromJSON(json),
|
.map((json) => createNotificationGroupFromJSON(json))
|
||||||
|
.filter(
|
||||||
|
// TODO: update notification groups instead of just filtering duplicates
|
||||||
|
(notification) =>
|
||||||
|
state.groups.every(
|
||||||
|
(group) =>
|
||||||
|
group.type === 'gap' ||
|
||||||
|
group.group_key !== notification.group_key,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (nextLink?.uri && notifications.length > 0) {
|
const sinceId = action.meta.arg.gap.sinceId;
|
||||||
|
if (
|
||||||
|
notifications.length > 0 &&
|
||||||
|
!(
|
||||||
|
oldestPageNotification &&
|
||||||
|
sinceId &&
|
||||||
|
compareId(oldestPageNotification, sinceId) <= 0
|
||||||
|
)
|
||||||
|
) {
|
||||||
// If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap
|
// If we get an empty page, it means we reached the bottom, so we do not need to insert a new gap
|
||||||
|
// Similarly, if we've fetched more than the gap's, this means we have completely filled it
|
||||||
toInsert.push({
|
toInsert.push({
|
||||||
type: 'gap',
|
type: 'gap',
|
||||||
loadUrl: nextLink.uri,
|
maxId: notifications.at(-1)?.page_max_id,
|
||||||
|
sinceId,
|
||||||
} as NotificationGap);
|
} as NotificationGap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: merge gaps made adjacent by moved/deleted groups
|
||||||
state.groups.splice(gapIndex, 1, ...toInsert);
|
state.groups.splice(gapIndex, 1, ...toInsert);
|
||||||
|
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
|
@ -162,14 +199,13 @@ export const notificationsGroupsReducer =
|
||||||
})
|
})
|
||||||
.addCase(disconnectTimeline, (state, action) => {
|
.addCase(disconnectTimeline, (state, action) => {
|
||||||
if (action.payload.timeline === 'home') {
|
if (action.payload.timeline === 'home') {
|
||||||
if (state.groups.length > 0 && state.groups[0]?.type === 'gap')
|
if (state.groups.length > 0 && state.groups[0]?.type !== 'gap') {
|
||||||
state.groups.shift();
|
|
||||||
|
|
||||||
state.groups.unshift({
|
state.groups.unshift({
|
||||||
type: 'gap',
|
type: 'gap',
|
||||||
loadUrl: 'TODO_LOAD_URL_TOP_OF_TL', // TODO
|
sinceId: state.groups[0]?.page_min_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addCase(timelineDelete, (state, action) => {
|
.addCase(timelineDelete, (state, action) => {
|
||||||
removeNotificationsForStatus(state, action.payload.statusId);
|
removeNotificationsForStatus(state, action.payload.statusId);
|
||||||
|
|
Loading…
Reference in New Issue