diff --git a/packages/pl-fe/src/actions/markers.ts b/packages/pl-fe/src/actions/markers.ts deleted file mode 100644 index 988236b73..000000000 --- a/packages/pl-fe/src/actions/markers.ts +++ /dev/null @@ -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) => - (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, -}; diff --git a/packages/pl-fe/src/actions/notifications.ts b/packages/pl-fe/src/actions/notifications.ts index a8e8dbad2..9a3311f8a 100644 --- a/packages/pl-fe/src/actions/notifications.ts +++ b/packages/pl-fe/src/actions/notifications.ts @@ -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, }; diff --git a/packages/pl-fe/src/api/hooks/streaming/useUserStream.ts b/packages/pl-fe/src/api/hooks/streaming/useUserStream.ts index 976db0d4a..fb962f459 100644 --- a/packages/pl-fe/src/api/hooks/streaming/useUserStream.ts +++ b/packages/pl-fe/src/api/hooks/streaming/useUserStream.ts @@ -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(['markers', key], marker)); break; } }, []); diff --git a/packages/pl-fe/src/features/notifications/index.tsx b/packages/pl-fe/src/features/notifications/index.tsx index 3df7e3042..0e0e7d1cd 100644 --- a/packages/pl-fe/src/features/notifications/index.tsx +++ b/packages/pl-fe/src/features/notifications/index.tsx @@ -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, }; - 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 | 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 = () => { ? () : 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) => ( { const scrollContainer = ( = ({ children }) => { })); prefetchNotifications(client, notificationsParams) - .then(() => dispatch(fetchMarker(['notifications']))) + .then(() => prefetchMarker(client, 'notifications')) .catch(console.error); if (account.is_admin || account.is_moderator) { diff --git a/packages/pl-fe/src/pl-hooks/hooks/markers/useMarkers.ts b/packages/pl-fe/src/pl-hooks/hooks/markers/useMarkers.ts new file mode 100644 index 000000000..7c4fda46d --- /dev/null +++ b/packages/pl-fe/src/pl-hooks/hooks/markers/useMarkers.ts @@ -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(['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 }; diff --git a/packages/pl-fe/src/pl-hooks/hooks/notifications/useNotifications.ts b/packages/pl-fe/src/pl-hooks/hooks/notifications/useNotificationList.ts similarity index 87% rename from packages/pl-fe/src/pl-hooks/hooks/notifications/useNotifications.ts rename to packages/pl-fe/src/pl-hooks/hooks/notifications/useNotificationList.ts index b8b6e92c2..9ccd33cc3 100644 --- a/packages/pl-fe/src/pl-hooks/hooks/notifications/useNotifications.ts +++ b/packages/pl-fe/src/pl-hooks/hooks/notifications/useNotificationList.ts @@ -33,7 +33,7 @@ const importNotifications = (response: PaginatedResponse) => { }; }; -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, 'previous' | 'next'>, - getNextPageParam: (response) => response, }); -export { useNotifications, prefetchNotifications }; +export { useNotificationList, prefetchNotifications }; diff --git a/packages/pl-fe/src/reducers/notifications.ts b/packages/pl-fe/src/reducers/notifications.ts index bf7b3a903..6c9ad4af7 100644 --- a/packages/pl-fe/src/reducers/notifications.ts +++ b/packages/pl-fe/src/reducers/notifications.ts @@ -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(), - hasMore: true, - top: false, unread: 0, - isLoading: false, queuedNotifications: ImmutableOrderedMap(), //max = MAX_QUEUED_NOTIFICATIONS totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+ - lastRead: -1 as string | -1, }); type State = ReturnType; @@ -122,18 +109,8 @@ const minifyNotification = (notification: Notification) => { type MinifiedNotification = ReturnType; -// Count how many notifications appear after the given ID (for unread count) -const countFuture = (notifications: ImmutableOrderedMap, 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, 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: