pl-fe: Remove notification queue from reducer

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-11-12 21:31:16 +01:00
parent dca8f15279
commit 74d0d4c60b
3 changed files with 45 additions and 101 deletions

View file

@ -24,7 +24,6 @@ import type { AppDispatch, RootState } from 'pl-fe/store';
const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE' as const;
const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP' as const;
const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE' as const;
const NOTIFICATIONS_DEQUEUE = 'NOTIFICATIONS_DEQUEUE' as const;
const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST' as const;
const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS' as const;
@ -100,8 +99,6 @@ const updateNotificationsQueue = (notification: BaseNotification, intlMessages:
let filtered: boolean | null = false;
const isOnNotificationsPage = curPath === '/notifications';
if (notification.type === 'mention' || notification.type === 'status') {
const regex = regexFromFilters(filters);
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
@ -139,37 +136,7 @@ const updateNotificationsQueue = (notification: BaseNotification, intlMessages:
});
}
if (isOnNotificationsPage) {
dispatch({
type: NOTIFICATIONS_UPDATE_QUEUE,
notification,
intlMessages,
intlLocale,
});
} else {
dispatch(updateNotifications(notification));
}
};
const dequeueNotifications = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
const queuedNotifications = getState().notifications.queuedNotifications;
const totalQueuedNotificationsCount = getState().notifications.totalQueuedNotificationsCount;
if (totalQueuedNotificationsCount === 0) {
return;
} else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
queuedNotifications.forEach((block) => {
dispatch(updateNotifications(block.notification));
});
} else {
dispatch(expandNotifications());
}
dispatch({
type: NOTIFICATIONS_DEQUEUE,
});
dispatch(markReadNotifications());
dispatch(updateNotifications(notification));
};
const excludeTypesFromFilter = (filters: string[]) => NOTIFICATION_TYPES.filter(item => !filters.includes(item));
@ -295,7 +262,6 @@ export {
NOTIFICATIONS_UPDATE,
NOTIFICATIONS_UPDATE_NOOP,
NOTIFICATIONS_UPDATE_QUEUE,
NOTIFICATIONS_DEQUEUE,
NOTIFICATIONS_EXPAND_REQUEST,
NOTIFICATIONS_EXPAND_SUCCESS,
NOTIFICATIONS_EXPAND_FAIL,
@ -309,7 +275,6 @@ export {
type FilterType,
updateNotifications,
updateNotificationsQueue,
dequeueNotifications,
expandNotifications,
expandNotificationsRequest,
expandNotificationsSuccess,

View file

@ -1,13 +1,13 @@
import clsx from 'clsx';
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { createSelector } from 'reselect';
import {
expandNotifications,
markReadNotifications,
scrollTopNotifications,
dequeueNotifications,
} from 'pl-fe/actions/notifications';
import PullToRefresh from 'pl-fe/components/pull-to-refresh';
import ScrollTopButton from 'pl-fe/components/scroll-top-button';
@ -31,7 +31,27 @@ const messages = defineMessages({
const getNotifications = createSelector([
(state: RootState) => state.notifications.items.toArray(),
], (notifications) => notifications.map(([_, notification]) => notification).filter(item => item !== null));
(_, topNotification?: string) => topNotification,
], (notifications, topNotificationId) => {
const allNotifications = notifications.map(([_, notification]) => notification).filter(item => item !== null);
if (topNotificationId) {
const queuedNotificationCount = allNotifications.findIndex((notification) =>
notification.most_recent_notification_id <= topNotificationId,
);
const displayedNotifications = allNotifications.slice(queuedNotificationCount);
return {
queuedNotificationCount,
displayedNotifications,
};
}
return {
queuedNotificationCount: 0,
displayedNotifications: allNotifications,
};
});
const Notifications = () => {
const dispatch = useAppDispatch();
@ -40,11 +60,11 @@ const Notifications = () => {
const showFilterBar = settings.notifications.quickFilter.show;
const activeFilter = settings.notifications.quickFilter.active;
const notifications = useAppSelector(state => getNotifications(state));
const [topNotification, setTopNotification] = useState<string>();
const { queuedNotificationCount, displayedNotifications } = useAppSelector(state => getNotifications(state, topNotification));
const isLoading = useAppSelector(state => state.notifications.isLoading);
// const isUnread = useAppSelector(state => state.notifications.unread > 0);
const hasMore = useAppSelector(state => state.notifications.hasMore);
const totalQueuedNotificationsCount = useAppSelector(state => state.notifications.totalQueuedNotificationsCount || 0);
const scrollableContentRef = useRef<Array<JSX.Element> | null>(null);
@ -53,26 +73,26 @@ const Notifications = () => {
// };
const handleLoadOlder = useCallback(debounce(() => {
const minId = notifications.reduce<string | undefined>(
const minId = displayedNotifications.reduce<string | undefined>(
(minId, notification) => minId && notification.page_min_id && notification.page_min_id > minId
? minId
: notification.page_min_id,
undefined,
);
dispatch(expandNotifications({ maxId: minId }));
}, 300, { leading: true }), [notifications]);
}, 300, { leading: true }), [displayedNotifications]);
const handleScroll = useCallback(debounce((startIndex?: number) => {
dispatch(scrollTopNotifications(startIndex === 0));
}, 100), []);
const handleMoveUp = (id: string) => {
const elementIndex = notifications.findIndex(item => item !== null && item.group_key === id) - 1;
const elementIndex = displayedNotifications.findIndex(item => item !== null && item.group_key === id) - 1;
_selectChild(elementIndex);
};
const handleMoveDown = (id: string) => {
const elementIndex = notifications.findIndex(item => item !== null && item.group_key === id) + 1;
const elementIndex = displayedNotifications.findIndex(item => item !== null && item.group_key === id) + 1;
_selectChild(elementIndex);
};
@ -84,7 +104,8 @@ const Notifications = () => {
};
const handleDequeueNotifications = useCallback(() => {
dispatch(dequeueNotifications());
setTopNotification(undefined);
dispatch(markReadNotifications());
}, []);
const handleRefresh = useCallback(() => dispatch(expandNotifications()), []);
@ -100,6 +121,11 @@ const Notifications = () => {
};
}, []);
useEffect(() => {
if (topNotification || displayedNotifications.length === 0) return;
setTopNotification(displayedNotifications[0].most_recent_notification_id);
}, [displayedNotifications.length]);
const emptyMessage = activeFilter === 'all'
? <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />
: <FormattedMessage id='empty_column.notifications_filtered' defaultMessage="You don't have any notifications of this type yet." />;
@ -112,8 +138,8 @@ const Notifications = () => {
if (isLoading && scrollableContentRef.current) {
scrollableContent = scrollableContentRef.current;
} else if (notifications.length > 0 || hasMore) {
scrollableContent = notifications.map((item) => (
} else if (displayedNotifications.length > 0 || hasMore) {
scrollableContent = displayedNotifications.map((item) => (
<Notification
key={item.group_key}
notification={item}
@ -130,7 +156,7 @@ const Notifications = () => {
const scrollContainer = (
<ScrollableList
isLoading={isLoading}
showLoading={isLoading && notifications.length === 0}
showLoading={isLoading && displayedNotifications.length === 0}
hasMore={hasMore}
emptyMessage={emptyMessage}
placeholderComponent={PlaceholderNotification}
@ -138,7 +164,7 @@ const Notifications = () => {
onLoadMore={handleLoadOlder}
onScroll={handleScroll}
listClassName={clsx('divide-y divide-solid divide-gray-200 black:divide-gray-800 dark:divide-primary-800', {
'animate-pulse': notifications.length === 0,
'animate-pulse': displayedNotifications.length === 0,
})}
>
{scrollableContent!}
@ -152,7 +178,7 @@ const Notifications = () => {
<Portal>
<ScrollTopButton
onClick={handleDequeueNotifications}
count={totalQueuedNotificationsCount}
count={queuedNotificationCount}
message={messages.queue}
/>
</Portal>

View file

@ -20,41 +20,29 @@ import {
NOTIFICATIONS_FILTER_SET,
NOTIFICATIONS_CLEAR,
NOTIFICATIONS_SCROLL_TOP,
NOTIFICATIONS_UPDATE_QUEUE,
NOTIFICATIONS_DEQUEUE,
NOTIFICATIONS_MARK_READ_REQUEST,
MAX_QUEUED_NOTIFICATIONS,
} from '../actions/notifications';
import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines';
import type { Notification as BaseNotification, Markers, NotificationGroup, PaginatedResponse, Relationship } from 'pl-api';
import type { AnyAction } from 'redux';
const QueuedNotificationRecord = ImmutableRecord({
notification: {} as any as BaseNotification,
intlMessages: {} as Record<string, string>,
intlLocale: '',
});
const ReducerRecord = ImmutableRecord({
items: ImmutableOrderedMap<string, NotificationGroup>(),
hasMore: true,
top: false,
unread: 0,
isLoading: false,
queuedNotifications: ImmutableOrderedMap<string, QueuedNotification>(), //max = MAX_QUEUED_NOTIFICATIONS
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
lastRead: -1 as string | -1,
});
type State = ReturnType<typeof ReducerRecord>;
type QueuedNotification = ReturnType<typeof QueuedNotificationRecord>;
const parseId = (id: string | number) => parseInt(id as string, 10);
// For sorting the notifications
const comparator = (a: Pick<NotificationGroup, 'group_key'>, b: Pick<NotificationGroup, 'group_key'>) => {
const parse = (m: Pick<NotificationGroup, 'group_key'>) => parseId(m.group_key);
const comparator = (a: Pick<NotificationGroup, 'most_recent_notification_id'>, b: Pick<NotificationGroup, 'most_recent_notification_id'>) => {
const parse = (m: Pick<NotificationGroup, 'most_recent_notification_id'>) => parseId(m.most_recent_notification_id);
if (parse(a) < parse(b)) return 1;
if (parse(a) > parse(b)) return -1;
return 0;
@ -75,13 +63,7 @@ const importNotification = (state: State, notification: NotificationGroup) => {
if (!top) state = state.update('unread', unread => unread + 1);
return state.update('items', map => {
if (top && map.size > 40) {
map = map.take(20);
}
return map.set(notification.group_key, notification).sort(comparator);
});
return state.update('items', map => map.set(notification.group_key, notification).sort(comparator));
};
const expandNormalizedNotifications = (state: State, notifications: NotificationGroup[], next: (() => Promise<PaginatedResponse<BaseNotification>>) | null) => {
@ -112,28 +94,6 @@ const deleteByStatus = (state: State, statusId: string) =>
// @ts-ignore
state.update('items', map => map.filterNot(item => item !== null && item.status === statusId));
const updateNotificationsQueue = (state: State, notification: BaseNotification, intlMessages: Record<string, string>, intlLocale: string) => {
const queuedNotifications = state.queuedNotifications;
const listedNotifications = state.items;
const totalQueuedNotificationsCount = state.totalQueuedNotificationsCount;
const alreadyExists = queuedNotifications.has(notification.group_key) || listedNotifications.has(notification.group_key);
if (alreadyExists) return state;
const newQueuedNotifications = queuedNotifications;
return state.withMutations(mutable => {
if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
mutable.set('queuedNotifications', newQueuedNotifications.set(notification.group_key, QueuedNotificationRecord({
notification,
intlMessages,
intlLocale,
})));
}
mutable.set('totalQueuedNotificationsCount', totalQueuedNotificationsCount + 1);
});
};
const importMarker = (state: State, marker: Markers) => {
const lastReadId = marker.notifications.last_read_id || -1 as string | -1;
@ -163,13 +123,6 @@ const notifications = (state: State = ReducerRecord(), action: AccountsAction |
return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE:
return importNotification(state, action.notification);
case NOTIFICATIONS_UPDATE_QUEUE:
return updateNotificationsQueue(state, action.notification, action.intlMessages, action.intlLocale);
case NOTIFICATIONS_DEQUEUE:
return state.withMutations(mutable => {
mutable.delete('queuedNotifications');
mutable.set('totalQueuedNotificationsCount', 0);
});
case NOTIFICATIONS_EXPAND_SUCCESS:
return expandNormalizedNotifications(state, action.notifications, action.next);
case ACCOUNT_BLOCK_SUCCESS: