mirror of https://github.com/mastodon/mastodon.git
Add slow mode support
parent
b4a8a18438
commit
d3f6612dfb
|
@ -1,3 +1,5 @@
|
||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
apiClearNotifications,
|
apiClearNotifications,
|
||||||
apiFetchNotifications,
|
apiFetchNotifications,
|
||||||
|
@ -109,6 +111,8 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const loadPending = createAction('notificationGroups/loadPending');
|
||||||
|
|
||||||
export const setNotificationsFilter = createAppAsyncThunk(
|
export const setNotificationsFilter = createAppAsyncThunk(
|
||||||
'notifications/filter/set',
|
'notifications/filter/set',
|
||||||
({ filterType }: { filterType: string }, { dispatch }) => {
|
({ filterType }: { filterType: string }, { dispatch }) => {
|
||||||
|
|
|
@ -13,13 +13,17 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?
|
||||||
import {
|
import {
|
||||||
fetchNotifications,
|
fetchNotifications,
|
||||||
fetchNotificationsGap,
|
fetchNotificationsGap,
|
||||||
|
loadPending,
|
||||||
} from 'mastodon/actions/notification_groups';
|
} 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 type { NotificationGap } from 'mastodon/reducers/notifications_groups';
|
||||||
import { selectUnreadNotificationsGroupsCount } from 'mastodon/selectors/notifications';
|
import {
|
||||||
|
selectUnreadNotificationsGroupsCount,
|
||||||
|
selectPendingNotificationsGroupsCount,
|
||||||
|
} from 'mastodon/selectors/notifications';
|
||||||
import {
|
import {
|
||||||
selectNeedsNotificationPermission,
|
selectNeedsNotificationPermission,
|
||||||
selectSettingsNotificationsExcludedTypes,
|
selectSettingsNotificationsExcludedTypes,
|
||||||
|
@ -34,7 +38,6 @@ import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { submitMarkers } from '../../actions/markers';
|
import { submitMarkers } from '../../actions/markers';
|
||||||
import {
|
import {
|
||||||
scrollTopNotifications,
|
scrollTopNotifications,
|
||||||
loadPending,
|
|
||||||
// mountNotifications,
|
// mountNotifications,
|
||||||
// unmountNotifications,
|
// unmountNotifications,
|
||||||
markNotificationsAsRead,
|
markNotificationsAsRead,
|
||||||
|
@ -80,16 +83,10 @@ const getNotifications = createSelector(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// const mapStateToProps = (state) => ({
|
|
||||||
// numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList())
|
|
||||||
// .size,
|
|
||||||
// });
|
|
||||||
|
|
||||||
export const Notifications: React.FC<{
|
export const Notifications: React.FC<{
|
||||||
columnId?: string;
|
columnId?: string;
|
||||||
multiColumn?: boolean;
|
multiColumn?: boolean;
|
||||||
numPending: number;
|
}> = ({ columnId, multiColumn }) => {
|
||||||
}> = ({ columnId, multiColumn, numPending }) => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const notifications = useAppSelector(getNotifications);
|
const notifications = useAppSelector(getNotifications);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
@ -100,6 +97,8 @@ export const Notifications: React.FC<{
|
||||||
selectSettingsNotificationsShowUnread(s) ? s.markers.notifications : '0',
|
selectSettingsNotificationsShowUnread(s) ? s.markers.notifications : '0',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const numPending = useAppSelector(selectPendingNotificationsGroupsCount);
|
||||||
|
|
||||||
const unreadNotificationsCount = useAppSelector(
|
const unreadNotificationsCount = useAppSelector(
|
||||||
selectUnreadNotificationsGroupsCount,
|
selectUnreadNotificationsGroupsCount,
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,12 +12,15 @@ import {
|
||||||
fetchNotifications,
|
fetchNotifications,
|
||||||
fetchNotificationsGap,
|
fetchNotificationsGap,
|
||||||
processNewNotificationForGroups,
|
processNewNotificationForGroups,
|
||||||
|
loadPending,
|
||||||
} from 'mastodon/actions/notification_groups';
|
} from 'mastodon/actions/notification_groups';
|
||||||
import {
|
import {
|
||||||
disconnectTimeline,
|
disconnectTimeline,
|
||||||
timelineDelete,
|
timelineDelete,
|
||||||
} from 'mastodon/actions/timelines_typed';
|
} from 'mastodon/actions/timelines_typed';
|
||||||
|
import type { ApiNotificationJSON } from 'mastodon/api_types/notifications';
|
||||||
import { compareId } from 'mastodon/compare_id';
|
import { compareId } from 'mastodon/compare_id';
|
||||||
|
import { usePendingItems } from 'mastodon/initial_state';
|
||||||
import {
|
import {
|
||||||
NOTIFICATIONS_GROUP_MAX_AVATARS,
|
NOTIFICATIONS_GROUP_MAX_AVATARS,
|
||||||
createNotificationGroupFromJSON,
|
createNotificationGroupFromJSON,
|
||||||
|
@ -33,22 +36,24 @@ export interface NotificationGap {
|
||||||
|
|
||||||
interface NotificationGroupsState {
|
interface NotificationGroupsState {
|
||||||
groups: (NotificationGroup | NotificationGap)[];
|
groups: (NotificationGroup | NotificationGap)[];
|
||||||
|
pendingGroups: (NotificationGroup | NotificationGap)[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: NotificationGroupsState = {
|
const initialState: NotificationGroupsState = {
|
||||||
groups: [],
|
groups: [],
|
||||||
|
pendingGroups: [], // holds pending groups in slow mode
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
hasMore: false,
|
hasMore: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function removeNotificationsForAccounts(
|
function filterNotificationsForAccounts(
|
||||||
state: NotificationGroupsState,
|
groups: NotificationGroupsState['groups'],
|
||||||
accountIds: string[],
|
accountIds: string[],
|
||||||
onlyForType?: string,
|
onlyForType?: string,
|
||||||
) {
|
) {
|
||||||
state.groups = state.groups
|
groups = groups
|
||||||
.map((group) => {
|
.map((group) => {
|
||||||
if (
|
if (
|
||||||
group.type !== 'gap' &&
|
group.type !== 'gap' &&
|
||||||
|
@ -71,20 +76,50 @@ function removeNotificationsForAccounts(
|
||||||
.filter(
|
.filter(
|
||||||
(group) => group.type === 'gap' || group.sampleAccountsIds.length > 0,
|
(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(
|
function removeNotificationsForStatus(
|
||||||
state: NotificationGroupsState,
|
state: NotificationGroupsState,
|
||||||
statusId: string,
|
statusId: string,
|
||||||
) {
|
) {
|
||||||
state.groups = state.groups.filter(
|
state.groups = filterNotificationsForStatus(state.groups, statusId);
|
||||||
(group) =>
|
state.pendingGroups = filterNotificationsForStatus(
|
||||||
group.type === 'gap' ||
|
state.pendingGroups,
|
||||||
!('statusId' in group) ||
|
statusId,
|
||||||
group.statusId !== statusId,
|
|
||||||
);
|
);
|
||||||
mergeGaps(state.groups);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNotificationGroup(
|
function isNotificationGroup(
|
||||||
|
@ -141,6 +176,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 =
|
export const notificationsGroupsReducer =
|
||||||
createReducer<NotificationGroupsState>(initialState, (builder) => {
|
createReducer<NotificationGroupsState>(initialState, (builder) => {
|
||||||
builder
|
builder
|
||||||
|
@ -232,48 +313,10 @@ export const notificationsGroupsReducer =
|
||||||
})
|
})
|
||||||
.addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
|
.addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
|
||||||
const notification = action.payload;
|
const notification = action.payload;
|
||||||
const existingGroupIndex = state.groups.findIndex(
|
processNewNotification(
|
||||||
(group) =>
|
usePendingItems ? state.pendingGroups : state.groups,
|
||||||
group.type !== 'gap' && group.group_key === notification.group_key,
|
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) => {
|
.addCase(disconnectTimeline, (state, action) => {
|
||||||
if (action.payload.timeline === 'home') {
|
if (action.payload.timeline === 'home') {
|
||||||
|
@ -290,6 +333,7 @@ export const notificationsGroupsReducer =
|
||||||
})
|
})
|
||||||
.addCase(clearNotifications.pending, (state) => {
|
.addCase(clearNotifications.pending, (state) => {
|
||||||
state.groups = [];
|
state.groups = [];
|
||||||
|
state.pendingGroups = [];
|
||||||
state.hasMore = false;
|
state.hasMore = false;
|
||||||
})
|
})
|
||||||
.addCase(blockAccountSuccess, (state, action) => {
|
.addCase(blockAccountSuccess, (state, action) => {
|
||||||
|
@ -307,6 +351,32 @@ export const notificationsGroupsReducer =
|
||||||
action.payload.accounts.map((account) => account.id),
|
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(
|
.addMatcher(
|
||||||
isAnyOf(authorizeFollowRequestSuccess, rejectFollowRequestSuccess),
|
isAnyOf(authorizeFollowRequestSuccess, rejectFollowRequestSuccess),
|
||||||
(state, action) => {
|
(state, action) => {
|
||||||
|
|
|
@ -6,14 +6,29 @@ import type { RootState } from 'mastodon/store';
|
||||||
export const selectUnreadNotificationsGroupsCount = createSelector(
|
export const selectUnreadNotificationsGroupsCount = createSelector(
|
||||||
[
|
[
|
||||||
(s: RootState) => s.markers.notifications,
|
(s: RootState) => s.markers.notifications,
|
||||||
|
(s: RootState) => s.notificationsGroups.pendingGroups,
|
||||||
(s: RootState) => s.notificationsGroups.groups,
|
(s: RootState) => s.notificationsGroups.groups,
|
||||||
],
|
],
|
||||||
(notificationMarker, groups) => {
|
(notificationMarker, pendingGroups, groups) => {
|
||||||
return groups.filter(
|
return (
|
||||||
(group) =>
|
groups.filter(
|
||||||
group.type !== 'gap' &&
|
(group) =>
|
||||||
group.page_max_id &&
|
group.type !== 'gap' &&
|
||||||
compareId(group.page_max_id, notificationMarker) > 0,
|
group.page_max_id &&
|
||||||
).length;
|
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,
|
||||||
|
);
|
||||||
|
|
Loading…
Reference in New Issue