Add slow mode support

Claire 2024-06-26 17:00:05 +02:00
parent 3b064b269f
commit 04e638349b
4 changed files with 155 additions and 67 deletions

View File

@ -1,3 +1,5 @@
import { createAction } from '@reduxjs/toolkit';
import {
apiClearNotifications,
apiFetchNotifications,
@ -109,6 +111,8 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
},
);
export const loadPending = createAction('notificationGroups/loadPending');
export const setNotificationsFilter = createAppAsyncThunk(
'notifications/filter/set',
({ filterType }: { filterType: string }, { dispatch }) => {

View File

@ -13,13 +13,17 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?
import {
fetchNotifications,
fetchNotificationsGap,
loadPending,
} from 'mastodon/actions/notification_groups';
import { compareId } from 'mastodon/compare_id';
import { Icon } from 'mastodon/components/icon';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import { useIdentity } from 'mastodon/identity_context';
import type { NotificationGap } from 'mastodon/reducers/notifications_groups';
import { selectUnreadNotificationsGroupsCount } from 'mastodon/selectors/notifications';
import {
selectUnreadNotificationsGroupsCount,
selectPendingNotificationsGroupsCount,
} from 'mastodon/selectors/notifications';
import {
selectNeedsNotificationPermission,
selectSettingsNotificationsExcludedTypes,
@ -34,7 +38,6 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { submitMarkers } from '../../actions/markers';
import {
scrollTopNotifications,
loadPending,
// mountNotifications,
// unmountNotifications,
markNotificationsAsRead,
@ -80,16 +83,10 @@ const getNotifications = createSelector(
},
);
// const mapStateToProps = (state) => ({
// numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList())
// .size,
// });
export const Notifications: React.FC<{
columnId?: string;
multiColumn?: boolean;
numPending: number;
}> = ({ columnId, multiColumn, numPending }) => {
}> = ({ columnId, multiColumn }) => {
const intl = useIntl();
const notifications = useAppSelector(getNotifications);
const dispatch = useAppDispatch();
@ -100,6 +97,8 @@ export const Notifications: React.FC<{
selectSettingsNotificationsShowUnread(s) ? s.markers.notifications : '0',
);
const numPending = useAppSelector(selectPendingNotificationsGroupsCount);
const unreadNotificationsCount = useAppSelector(
selectUnreadNotificationsGroupsCount,
);

View File

@ -12,12 +12,15 @@ import {
fetchNotifications,
fetchNotificationsGap,
processNewNotificationForGroups,
loadPending,
} from 'mastodon/actions/notification_groups';
import {
disconnectTimeline,
timelineDelete,
} from 'mastodon/actions/timelines_typed';
import type { ApiNotificationJSON } from 'mastodon/api_types/notifications';
import { compareId } from 'mastodon/compare_id';
import { usePendingItems } from 'mastodon/initial_state';
import {
NOTIFICATIONS_GROUP_MAX_AVATARS,
createNotificationGroupFromJSON,
@ -33,20 +36,22 @@ export interface NotificationGap {
interface NotificationGroupsState {
groups: (NotificationGroup | NotificationGap)[];
pendingGroups: (NotificationGroup | NotificationGap)[];
isLoading: boolean;
}
const initialState: NotificationGroupsState = {
groups: [],
pendingGroups: [], // holds pending groups in slow mode
isLoading: false,
};
function removeNotificationsForAccounts(
state: NotificationGroupsState,
function filterNotificationsForAccounts(
groups: NotificationGroupsState['groups'],
accountIds: string[],
onlyForType?: string,
) {
state.groups = state.groups
groups = groups
.map((group) => {
if (
group.type !== 'gap' &&
@ -69,20 +74,50 @@ function removeNotificationsForAccounts(
.filter(
(group) => group.type === 'gap' || group.sampleAccountsIds.length > 0,
);
mergeGaps(state.groups);
mergeGaps(groups);
return groups;
}
function filterNotificationsForStatus(
groups: NotificationGroupsState['groups'],
statusId: string,
) {
groups = groups.filter(
(group) =>
group.type === 'gap' ||
!('statusId' in group) ||
group.statusId !== statusId,
);
mergeGaps(groups);
return groups;
}
function removeNotificationsForAccounts(
state: NotificationGroupsState,
accountIds: string[],
onlyForType?: string,
) {
state.groups = filterNotificationsForAccounts(
state.groups,
accountIds,
onlyForType,
);
state.pendingGroups = filterNotificationsForAccounts(
state.pendingGroups,
accountIds,
onlyForType,
);
}
function removeNotificationsForStatus(
state: NotificationGroupsState,
statusId: string,
) {
state.groups = state.groups.filter(
(group) =>
group.type === 'gap' ||
!('statusId' in group) ||
group.statusId !== statusId,
state.groups = filterNotificationsForStatus(state.groups, statusId);
state.pendingGroups = filterNotificationsForStatus(
state.pendingGroups,
statusId,
);
mergeGaps(state.groups);
}
function isNotificationGroup(
@ -139,6 +174,52 @@ function mergeGapsAround(
}
}
function processNewNotification(
groups: NotificationGroupsState['groups'],
notification: ApiNotificationJSON,
) {
const existingGroupIndex = groups.findIndex(
(group) =>
group.type !== 'gap' && group.group_key === notification.group_key,
);
// In any case, we are going to add a group at the top
// If there is currently a gap at the top, now is the time to update it
if (groups.length > 0 && groups[0]?.type === 'gap') {
groups[0].maxId = notification.id;
}
if (existingGroupIndex > -1) {
const existingGroup = groups[existingGroupIndex];
if (
existingGroup &&
existingGroup.type !== 'gap' &&
!existingGroup.sampleAccountsIds.includes(notification.account.id) // This can happen for example if you like, then unlike, then like again the same post
) {
// Update the existing group
if (
existingGroup.sampleAccountsIds.unshift(notification.account.id) >
NOTIFICATIONS_GROUP_MAX_AVATARS
)
existingGroup.sampleAccountsIds.pop();
existingGroup.most_recent_notification_id = notification.id;
existingGroup.page_max_id = notification.id;
existingGroup.latest_page_notification_at = notification.created_at;
existingGroup.notifications_count += 1;
groups.splice(existingGroupIndex, 1);
mergeGapsAround(groups, existingGroupIndex);
groups.unshift(existingGroup);
}
} else {
// Create a new group
groups.unshift(createNotificationGroupFromNotificationJSON(notification));
}
}
export const notificationsGroupsReducer =
createReducer<NotificationGroupsState>(initialState, (builder) => {
builder
@ -230,48 +311,10 @@ export const notificationsGroupsReducer =
})
.addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
const notification = action.payload;
const existingGroupIndex = state.groups.findIndex(
(group) =>
group.type !== 'gap' && group.group_key === notification.group_key,
processNewNotification(
usePendingItems ? state.pendingGroups : state.groups,
notification,
);
// In any case, we are going to add a group at the top
// If there is currently a gap at the top, now is the time to update it
if (state.groups.length > 0 && state.groups[0]?.type === 'gap') {
state.groups[0].maxId = notification.id;
}
if (existingGroupIndex > -1) {
const existingGroup = state.groups[existingGroupIndex];
if (
existingGroup &&
existingGroup.type !== 'gap' &&
!existingGroup.sampleAccountsIds.includes(notification.account.id) // This can happen for example if you like, then unlike, then like again the same post
) {
// Update the existing group
if (
existingGroup.sampleAccountsIds.unshift(notification.account.id) >
NOTIFICATIONS_GROUP_MAX_AVATARS
)
existingGroup.sampleAccountsIds.pop();
existingGroup.most_recent_notification_id = notification.id;
existingGroup.page_max_id = notification.id;
existingGroup.latest_page_notification_at = notification.created_at;
existingGroup.notifications_count += 1;
state.groups.splice(existingGroupIndex, 1);
mergeGapsAround(state.groups, existingGroupIndex);
state.groups.unshift(existingGroup);
}
} else {
// Create a new group
state.groups.unshift(
createNotificationGroupFromNotificationJSON(notification),
);
}
})
.addCase(disconnectTimeline, (state, action) => {
if (action.payload.timeline === 'home') {
@ -288,6 +331,7 @@ export const notificationsGroupsReducer =
})
.addCase(clearNotifications.pending, (state) => {
state.groups = [];
state.pendingGroups = [];
})
.addCase(blockAccountSuccess, (state, action) => {
removeNotificationsForAccounts(state, [action.payload.relationship.id]);
@ -304,6 +348,32 @@ export const notificationsGroupsReducer =
action.payload.accounts.map((account) => account.id),
);
})
.addCase(loadPending, (state) => {
// First, remove any existing group and merge data
state.pendingGroups.forEach((group) => {
if (group.type !== 'gap') {
const existingGroupIndex = state.groups.findIndex(
(groupOrGap) =>
isNotificationGroup(groupOrGap) &&
groupOrGap.group_key === group.group_key,
);
if (existingGroupIndex > -1) {
const existingGroup = state.groups[existingGroupIndex];
if (existingGroup && existingGroup.type !== 'gap') {
group.notifications_count += existingGroup.notifications_count;
group.sampleAccountsIds = group.sampleAccountsIds
.concat(existingGroup.sampleAccountsIds)
.slice(0, NOTIFICATIONS_GROUP_MAX_AVATARS);
state.groups.splice(existingGroupIndex, 1);
}
}
}
});
// Then build the consolidated list and clear pending groups
state.groups = state.pendingGroups.concat(state.groups);
state.pendingGroups = [];
})
.addMatcher(
isAnyOf(authorizeFollowRequestSuccess, rejectFollowRequestSuccess),
(state, action) => {

View File

@ -6,14 +6,29 @@ import type { RootState } from 'mastodon/store';
export const selectUnreadNotificationsGroupsCount = createSelector(
[
(s: RootState) => s.markers.notifications,
(s: RootState) => s.notificationsGroups.pendingGroups,
(s: RootState) => s.notificationsGroups.groups,
],
(notificationMarker, groups) => {
return groups.filter(
(group) =>
group.type !== 'gap' &&
group.page_max_id &&
compareId(group.page_max_id, notificationMarker) > 0,
).length;
(notificationMarker, pendingGroups, groups) => {
return (
groups.filter(
(group) =>
group.type !== 'gap' &&
group.page_max_id &&
compareId(group.page_max_id, notificationMarker) > 0,
).length +
pendingGroups.filter(
(group) =>
group.type !== 'gap' &&
group.page_max_id &&
compareId(group.page_max_id, notificationMarker) > 0,
).length
);
},
);
export const selectPendingNotificationsGroupsCount = createSelector(
[(s: RootState) => s.notificationsGroups.pendingGroups],
(pendingGroups) =>
pendingGroups.filter((group) => group.type !== 'gap').length,
);