wip hooks migration

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2024-09-29 18:07:55 +02:00
parent 1db9b0f75c
commit f2fba8e4db
8 changed files with 86 additions and 177 deletions

View File

@ -1,43 +0,0 @@
import { getClient } from 'pl-fe/api';
import type { SaveMarkersParams } from 'pl-api';
import type { AppDispatch, RootState } from 'pl-fe/store';
const MARKER_FETCH_REQUEST = 'MARKER_FETCH_REQUEST' as const;
const MARKER_FETCH_SUCCESS = 'MARKER_FETCH_SUCCESS' as const;
const MARKER_FETCH_FAIL = 'MARKER_FETCH_FAIL' as const;
const MARKER_SAVE_REQUEST = 'MARKER_SAVE_REQUEST' as const;
const MARKER_SAVE_SUCCESS = 'MARKER_SAVE_SUCCESS' as const;
const MARKER_SAVE_FAIL = 'MARKER_SAVE_FAIL' as const;
const fetchMarker = (timeline: Array<string>) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: MARKER_FETCH_REQUEST });
return getClient(getState).timelines.getMarkers(timeline).then((marker) => {
dispatch({ type: MARKER_FETCH_SUCCESS, marker });
}).catch(error => {
dispatch({ type: MARKER_FETCH_FAIL, error });
});
};
const saveMarker = (marker: SaveMarkersParams) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: MARKER_SAVE_REQUEST, marker });
return getClient(getState).timelines.saveMarkers(marker).then((marker) => {
dispatch({ type: MARKER_SAVE_SUCCESS, marker });
}).catch(error => {
dispatch({ type: MARKER_SAVE_FAIL, error });
});
};
export {
MARKER_FETCH_REQUEST,
MARKER_FETCH_SUCCESS,
MARKER_FETCH_FAIL,
MARKER_SAVE_REQUEST,
MARKER_SAVE_SUCCESS,
MARKER_SAVE_FAIL,
fetchMarker,
saveMarker,
};

View File

@ -6,13 +6,10 @@ import { getNotificationStatus } from 'pl-fe/features/notifications/components/n
import { normalizeNotification } from 'pl-fe/normalizers';
import { importEntities } from 'pl-fe/pl-hooks/importer';
import { getFilters, regexFromFilters } from 'pl-fe/selectors';
import { isLoggedIn } from 'pl-fe/utils/auth';
import { compareId } from 'pl-fe/utils/comparators';
import { unescapeHTML } from 'pl-fe/utils/html';
import { joinPublicPath } from 'pl-fe/utils/static';
import { fetchRelationships } from './accounts';
import { saveMarker } from './markers';
import { getSettings, saveSettings } from './settings';
import type { Notification } from 'pl-api';
@ -25,13 +22,6 @@ const NOTIFICATIONS_DEQUEUE = 'NOTIFICATIONS_DEQUEUE' as const;
const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET' as const;
const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR' as const;
const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP' as const;
const NOTIFICATIONS_MARK_READ_REQUEST = 'NOTIFICATIONS_MARK_READ_REQUEST' as const;
const NOTIFICATIONS_MARK_READ_SUCCESS = 'NOTIFICATIONS_MARK_READ_SUCCESS' as const;
const NOTIFICATIONS_MARK_READ_FAIL = 'NOTIFICATIONS_MARK_READ_FAIL' as const;
const MAX_QUEUED_NOTIFICATIONS = 40;
type FILTER_TYPES = {
@ -156,16 +146,7 @@ const dequeueNotifications = () =>
dispatch({
type: NOTIFICATIONS_DEQUEUE,
});
dispatch(markReadNotifications());
};
const scrollTopNotifications = (top: boolean) =>
(dispatch: AppDispatch) => {
dispatch({
type: NOTIFICATIONS_SCROLL_TOP,
top,
});
dispatch(markReadNotifications());
// dispatch(markReadNotifications());
};
const setFilter = (filterType: FilterType, abort?: boolean) =>
@ -180,42 +161,16 @@ const setFilter = (filterType: FilterType, abort?: boolean) =>
if (activeFilter !== filterType) dispatch(saveSettings());
};
const markReadNotifications = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
const topNotificationId = state.notifications.items.first()?.id;
const lastReadId = state.notifications.lastRead;
if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) {
const marker = {
notifications: {
last_read_id: topNotificationId,
},
};
dispatch(saveMarker(marker));
}
};
export {
NOTIFICATIONS_UPDATE,
NOTIFICATIONS_UPDATE_NOOP,
NOTIFICATIONS_UPDATE_QUEUE,
NOTIFICATIONS_DEQUEUE,
NOTIFICATIONS_FILTER_SET,
NOTIFICATIONS_CLEAR,
NOTIFICATIONS_SCROLL_TOP,
NOTIFICATIONS_MARK_READ_REQUEST,
NOTIFICATIONS_MARK_READ_SUCCESS,
NOTIFICATIONS_MARK_READ_FAIL,
MAX_QUEUED_NOTIFICATIONS,
type FilterType,
updateNotifications,
updateNotificationsQueue,
dequeueNotifications,
scrollTopNotifications,
setFilter,
markReadNotifications,
};

View File

@ -2,7 +2,6 @@ import { useCallback } from 'react';
import { updateConversations } from 'pl-fe/actions/conversations';
import { fetchFilters } from 'pl-fe/actions/filters';
import { MARKER_FETCH_SUCCESS } from 'pl-fe/actions/markers';
import { updateNotificationsQueue } from 'pl-fe/actions/notifications';
import { getLocale, getSettings } from 'pl-fe/actions/settings';
import { updateStatus } from 'pl-fe/actions/statuses';
@ -21,7 +20,7 @@ import { updateReactions } from '../announcements/useAnnouncements';
import { useTimelineStream } from './useTimelineStream';
import type { Announcement, AnnouncementReaction, FollowRelationshipUpdate, Relationship, StreamingEvent } from 'pl-api';
import type { Announcement, AnnouncementReaction, FollowRelationshipUpdate, Marker, Relationship, StreamingEvent } from 'pl-api';
import type { AppDispatch, RootState } from 'pl-fe/store';
const updateAnnouncementReactions = ({ announcement_id: id, name }: AnnouncementReaction) => {
@ -169,7 +168,7 @@ const useUserStream = () => {
deleteAnnouncement(event.payload);
break;
case 'marker':
dispatch({ type: MARKER_FETCH_SUCCESS, marker: event.payload });
Object.entries(event.payload).forEach(([key, marker]) => queryClient.setQueryData<Marker>(['markers', key], marker));
break;
}
}, []);

View File

@ -3,17 +3,16 @@ import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useRef } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import {
scrollTopNotifications,
dequeueNotifications,
} from 'pl-fe/actions/notifications';
import { dequeueNotifications } from 'pl-fe/actions/notifications';
import PullToRefresh from 'pl-fe/components/pull-to-refresh';
import ScrollTopButton from 'pl-fe/components/scroll-top-button';
import ScrollableList from 'pl-fe/components/scrollable-list';
import { Column, Portal } from 'pl-fe/components/ui';
import PlaceholderNotification from 'pl-fe/features/placeholder/components/placeholder-notification';
import { useAppDispatch, useAppSelector, useSettings } from 'pl-fe/hooks';
import { useNotifications } from 'pl-fe/pl-hooks/hooks/notifications/useNotifications';
import { useMarker, useUpdateMarkerMutation } from 'pl-fe/pl-hooks/hooks/markers/useMarkers';
import { useNotificationList } from 'pl-fe/pl-hooks/hooks/notifications/useNotificationList';
import { compareId } from 'pl-fe/utils/comparators';
import { NotificationType } from 'pl-fe/utils/notification';
import FilterBar from './components/filter-bar';
@ -42,15 +41,19 @@ const Notifications = () => {
const intl = useIntl();
const settings = useSettings();
const activeFilter = settings.notifications.quickFilter.active as FilterType;
const params = activeFilter === 'all' ? {} : {
types: FILTER_TYPES[activeFilter] || [activeFilter] as Array<NotificationType>,
};
const notificationsQuery = useNotifications(params);
const notificationListQuery = useNotificationList(params);
const notifications = notificationsQuery.data;
const markerQuery = useMarker('notifications');
const updateMarkerMutation = useUpdateMarkerMutation('notifications');
const notifications = notificationListQuery.data;
const showFilterBar = settings.notifications.quickFilter.show;
const totalQueuedNotificationsCount = useAppSelector(state => state.notifications.totalQueuedNotificationsCount || 0);
@ -59,12 +62,23 @@ const Notifications = () => {
const scrollableContentRef = useRef<Array<JSX.Element> | null>(null);
const handleLoadOlder = useCallback(debounce(() => {
if (notificationsQuery.hasNextPage) notificationsQuery.fetchNextPage();
}, 300, { leading: true }), [notificationsQuery.hasNextPage]);
if (notificationListQuery.hasNextPage) notificationListQuery.fetchNextPage();
}, 300, { leading: true }), [notificationListQuery.hasNextPage]);
const handleScrollToTop = () => {
const topNotificationId = notificationListQuery.data[0];
const lastReadId = markerQuery.data?.last_read_id || -1;
if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) {
updateMarkerMutation.mutate(topNotificationId);
}
};
const handleScroll = useCallback(debounce((startIndex?: number) => {
dispatch(scrollTopNotifications(startIndex === 0));
}, 100), []);
if (startIndex !== 0) return;
handleScrollToTop();
}, 100), [handleScrollToTop]);
const handleMoveUp = (id: string) => {
const elementIndex = notifications.findIndex(item => item !== null && item === id) - 1;
@ -87,16 +101,15 @@ const Notifications = () => {
dispatch(dequeueNotifications());
}, []);
const handleRefresh = useCallback(() => notificationsQuery.refetch(), []);
const handleRefresh = useCallback(() => notificationListQuery.refetch(), []);
useEffect(() => {
handleDequeueNotifications();
dispatch(scrollTopNotifications(true));
handleScrollToTop();
return () => {
handleLoadOlder.cancel();
handleScroll.cancel();
dispatch(scrollTopNotifications(false));
};
}, []);
@ -110,9 +123,9 @@ const Notifications = () => {
? (<FilterBar />)
: null;
if (notificationsQuery.isLoading && scrollableContentRef.current) {
if (notificationListQuery.isLoading && scrollableContentRef.current) {
scrollableContent = scrollableContentRef.current;
} else if (notifications.length > 0 || notificationsQuery.hasNextPage) {
} else if (notifications.length > 0 || notificationListQuery.hasNextPage) {
scrollableContent = notifications.map((notificationId) => (
<Notification
key={notificationId}
@ -129,9 +142,9 @@ const Notifications = () => {
const scrollContainer = (
<ScrollableList
isLoading={notificationsQuery.isFetching}
showLoading={notificationsQuery.isLoading}
hasMore={notificationsQuery.hasNextPage}
isLoading={notificationListQuery.isFetching}
showLoading={notificationListQuery.isLoading}
hasMore={notificationListQuery.hasNextPage}
emptyMessage={emptyMessage}
placeholderComponent={PlaceholderNotification}
placeholderCount={20}

View File

@ -7,7 +7,6 @@ import { fetchReports, fetchUsers, fetchConfig } from 'pl-fe/actions/admin';
import { fetchCustomEmojis } from 'pl-fe/actions/custom-emojis';
import { fetchDraftStatuses } from 'pl-fe/actions/draft-statuses';
import { fetchFilters } from 'pl-fe/actions/filters';
import { fetchMarker } from 'pl-fe/actions/markers';
import { FilterType } from 'pl-fe/actions/notifications';
import { register as registerPushNotifications } from 'pl-fe/actions/push-notifications';
import { fetchScheduledStatuses } from 'pl-fe/actions/scheduled-statuses';
@ -34,7 +33,8 @@ import ProfileLayout from 'pl-fe/layouts/profile-layout';
import RemoteInstanceLayout from 'pl-fe/layouts/remote-instance-layout';
import SearchLayout from 'pl-fe/layouts/search-layout';
import StatusLayout from 'pl-fe/layouts/status-layout';
import { prefetchNotifications } from 'pl-fe/pl-hooks/hooks/notifications/useNotifications';
import { prefetchMarker } from 'pl-fe/pl-hooks/hooks/markers/useMarkers';
import { prefetchNotifications } from 'pl-fe/pl-hooks/hooks/notifications/useNotificationList';
import { useUiStore } from 'pl-fe/stores';
import { getVapidKey } from 'pl-fe/utils/auth';
import { isStandalone } from 'pl-fe/utils/state';
@ -391,7 +391,7 @@ const UI: React.FC<IUI> = ({ children }) => {
}));
prefetchNotifications(client, notificationsParams)
.then(() => dispatch(fetchMarker(['notifications'])))
.then(() => prefetchMarker(client, 'notifications'))
.catch(console.error);
if (account.is_admin || account.is_moderator) {

View File

@ -0,0 +1,42 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { useClient } from 'pl-fe/hooks';
import { queryClient } from 'pl-fe/queries/client';
import type { Marker, PlApiClient } from 'pl-api';
type Timeline = 'home' | 'notifications';
const useMarker = (timeline: Timeline) => {
const client = useClient();
return useQuery({
queryKey: ['markers', timeline],
queryFn: () => client.timelines.getMarkers([timeline]).then(markers => markers[timeline]),
});
};
const useUpdateMarkerMutation = (timeline: Timeline) => {
const client = useClient();
return useMutation({
mutationFn: (lastReadId: string) => client.timelines.saveMarkers({
[timeline]: {
last_read_id: lastReadId,
},
}),
retry: false,
onMutate: (lastReadId) => queryClient.setQueryData<Marker>(['markers', timeline], (marker) => marker ? ({
...marker,
last_read_id: lastReadId,
}) : undefined),
});
};
const prefetchMarker = (client: PlApiClient, timeline: 'home' | 'notifications') =>
queryClient.prefetchQuery({
queryKey: ['markers', timeline],
queryFn: () => client.timelines.getMarkers([timeline]).then(markers => markers[timeline]),
});
export { useMarker, prefetchMarker, useUpdateMarkerMutation };

View File

@ -33,7 +33,7 @@ const importNotifications = (response: PaginatedResponse<BaseNotification>) => {
};
};
const useNotifications = (params: UseNotificationParams) => {
const useNotificationList = (params: UseNotificationParams) => {
const client = useClient();
const notificationsQuery = useInfiniteQuery({
@ -57,12 +57,11 @@ const useNotifications = (params: UseNotificationParams) => {
const prefetchNotifications = (client: PlApiClient, params: UseNotificationParams) =>
queryClient.prefetchInfiniteQuery({
queryKey: getQueryKey(params),
queryFn: ({ pageParam }) => (pageParam.next ? pageParam.next() : client.notifications.getNotifications({
queryFn: () => client.notifications.getNotifications({
types: params.types,
exclude_types: params.excludeTypes,
})).then(importNotifications),
}).then(importNotifications),
initialPageParam: { previous: null, next: null } as Pick<PaginatedResponse<BaseNotification>, 'previous' | 'next'>,
getNextPageParam: (response) => response,
});
export { useNotifications, prefetchNotifications };
export { useNotificationList, prefetchNotifications };

View File

@ -7,24 +7,15 @@ import {
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
FOLLOW_REQUEST_REJECT_SUCCESS,
} from '../actions/accounts';
import {
MARKER_FETCH_SUCCESS,
MARKER_SAVE_REQUEST,
MARKER_SAVE_SUCCESS,
} from '../actions/markers';
import {
NOTIFICATIONS_UPDATE,
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 } from '../actions/timelines';
import type { AccountWarning, Notification as BaseNotification, Markers, Relationship, RelationshipSeveranceEvent, Report } from 'pl-api';
import type { AccountWarning, Notification as BaseNotification, Relationship, RelationshipSeveranceEvent, Report } from 'pl-api';
import type { Notification } from 'pl-fe/normalizers';
import type { AnyAction } from 'redux';
@ -36,13 +27,9 @@ const QueuedNotificationRecord = ImmutableRecord({
const ReducerRecord = ImmutableRecord({
items: ImmutableOrderedMap<string, MinifiedNotification>(),
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>;
@ -122,18 +109,8 @@ const minifyNotification = (notification: Notification) => {
type MinifiedNotification = ReturnType<typeof minifyNotification>;
// Count how many notifications appear after the given ID (for unread count)
const countFuture = (notifications: ImmutableOrderedMap<string, MinifiedNotification>, lastId: string | number) =>
notifications.reduce((acc, notification) => {
if (!notification.duplicate && parseId(notification.id) > parseId(lastId)) {
return acc + 1;
} else {
return acc;
}
}, 0);
const importNotification = (state: State, notification: Notification) => {
const top = state.top;
const top = false; // state.top;
if (!top && !notification.duplicate) state = state.update('unread', unread => unread + 1);
@ -154,11 +131,6 @@ const filterNotificationIds = (state: State, accountIds: Array<string>, type?: s
return state.update('items', helper);
};
const updateTop = (state: State, top: boolean) => {
if (top) state = state.set('unread', 0);
return state.set('top', top);
};
const deleteByStatus = (state: State, statusId: string) =>
// @ts-ignore
state.update('items', map => map.filterNot(item => item !== null && item.status === statusId));
@ -185,28 +157,8 @@ const updateNotificationsQueue = (state: State, notification: BaseNotification,
});
};
const importMarker = (state: State, marker: Markers) => {
const lastReadId = marker.notifications.last_read_id || -1 as string | -1;
if (!lastReadId) {
return state;
}
return state.withMutations(state => {
const notifications = state.items;
const unread = countFuture(notifications, lastReadId);
state.set('unread', unread);
state.set('lastRead', lastReadId);
});
};
const notifications = (state: State = ReducerRecord(), action: AnyAction) => {
switch (action.type) {
case NOTIFICATIONS_FILTER_SET:
return state.set('items', ImmutableOrderedMap()).set('hasMore', true);
case NOTIFICATIONS_SCROLL_TOP:
return updateTop(state, action.top);
case NOTIFICATIONS_UPDATE:
return importNotification(state, action.notification);
case NOTIFICATIONS_UPDATE_QUEUE:
@ -223,14 +175,6 @@ const notifications = (state: State = ReducerRecord(), action: AnyAction) => {
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
case FOLLOW_REQUEST_REJECT_SUCCESS:
return filterNotificationIds(state, [action.accountId], 'follow_request');
case NOTIFICATIONS_CLEAR:
return state.set('items', ImmutableOrderedMap()).set('hasMore', false);
case NOTIFICATIONS_MARK_READ_REQUEST:
return state.set('lastRead', action.lastRead);
case MARKER_FETCH_SUCCESS:
case MARKER_SAVE_REQUEST:
case MARKER_SAVE_SUCCESS:
return importMarker(state, action.marker);
case TIMELINE_DELETE:
return deleteByStatus(state, action.statusId);
default: