diff --git a/src/shared/hooks/useCases/useInboxItems.ts b/src/shared/hooks/useCases/useInboxItems.ts index ea46d4d7f..9e15e5508 100644 --- a/src/shared/hooks/useCases/useInboxItems.ts +++ b/src/shared/hooks/useCases/useInboxItems.ts @@ -5,6 +5,7 @@ import { Logger, UserService } from "@/services"; import { addMetadataToItemsBatch } from "@/services/utils"; import { checkIsFeedItemFollowLayoutItemWithFollowData, + FeedLayoutItemWithFollowData, InboxItemsBatch as ItemsBatch, } from "@/shared/interfaces"; import { InboxItem, Timestamp } from "@/shared/models"; @@ -21,6 +22,46 @@ interface Return refetch: () => void; } +const filterItemsInTheMiddle = (info: { + fetchedInboxItems: InboxItem[]; + unread: boolean; + currentData: FeedLayoutItemWithFollowData[] | null; +}): { addedItems: InboxItem[]; removedItems: InboxItem[] } => { + const { fetchedInboxItems, unread, currentData } = info; + const newItems = currentData + ? fetchedInboxItems.filter((fetchedItem) => + currentData.every((item) => + checkIsFeedItemFollowLayoutItemWithFollowData(item) + ? item.feedItemFollowWithMetadata.id !== fetchedItem.itemId + : item.itemId !== fetchedItem.itemId, + ), + ) + : fetchedInboxItems; + + if (!unread) { + return { + addedItems: newItems, + removedItems: [], + }; + } + + const removedItems = fetchedInboxItems.filter( + (fetchedItem) => + !fetchedItem.unread && + (currentData || []).some((item) => + checkIsFeedItemFollowLayoutItemWithFollowData(item) + ? item.feedItemFollowWithMetadata.id === fetchedItem.itemId + : item.itemId === fetchedItem.itemId, + ), + ); + const filteredItems = newItems.filter((item) => item.unread); + + return { + addedItems: filteredItems, + removedItems, + }; +}; + export const useInboxItems = ( feedItemIdsForNotListening?: string[], options?: { unread?: boolean }, @@ -76,6 +117,7 @@ export const useInboxItems = ( data, firstDocTimestamp: startAt, lastDocTimestamp: endAt, + unread, } = inboxItems; if (!userId || !startAt || !endAt) { @@ -88,29 +130,34 @@ export const useInboxItems = ( endAt, }); - if (!isMounted || inboxItemsRef.current.unread) { + if (!isMounted) { return; } - const filteredItems = data - ? fetchedInboxItems.filter((fetchedItem) => - data.every((item) => - checkIsFeedItemFollowLayoutItemWithFollowData(item) - ? item.feedItemFollowWithMetadata.id !== fetchedItem.itemId - : item.itemId !== fetchedItem.itemId, - ), - ) - : fetchedInboxItems; - - addNewInboxItems( - filteredItems.map((item) => ({ - item, - statuses: { - isAdded: false, - isRemoved: false, - }, - })), - ); + const { addedItems, removedItems } = filterItemsInTheMiddle({ + fetchedInboxItems, + unread, + currentData: data, + }); + const addedItemsWithStatuses = addedItems.map((item) => ({ + item, + statuses: { + isAdded: false, + isRemoved: false, + }, + })); + const removedItemsWithStatuses = removedItems.map((item) => ({ + item, + statuses: { + isAdded: false, + isRemoved: true, + }, + })); + + addNewInboxItems([ + ...addedItemsWithStatuses, + ...removedItemsWithStatuses, + ]); } catch (err) { Logger.error(err); } @@ -119,7 +166,7 @@ export const useInboxItems = ( return () => { isMounted = false; }; - }, []); + }, [inboxItems.unread]); useEffect(() => { if (!inboxItems.firstDocTimestamp || !userId) { diff --git a/src/store/states/inbox/actions.ts b/src/store/states/inbox/actions.ts index a1978f4bc..77f59adae 100644 --- a/src/store/states/inbox/actions.ts +++ b/src/store/states/inbox/actions.ts @@ -17,6 +17,7 @@ export const getInboxItems = createAsyncAction( { limit?: number; unread?: boolean; + shouldUseLastStateIfExists?: boolean; }, Omit, Error, @@ -114,3 +115,7 @@ export const addChatChannelItem = createStandardAction( export const removeEmptyChatChannelItems = createStandardAction( InboxActionType.REMOVE_EMPTY_CHAT_CHANNEL_ITEMS, )(); + +export const saveLastState = createStandardAction( + InboxActionType.SAVE_LAST_STATE, +)<{ shouldSaveAsReadState: boolean }>(); diff --git a/src/store/states/inbox/constants.ts b/src/store/states/inbox/constants.ts index 19f8d794a..6d5e907ee 100644 --- a/src/store/states/inbox/constants.ts +++ b/src/store/states/inbox/constants.ts @@ -31,4 +31,6 @@ export enum InboxActionType { ADD_CHAT_CHANNEL_ITEM = "@INBOX/ADD_CHAT_CHANNEL_ITEM", REMOVE_EMPTY_CHAT_CHANNEL_ITEMS = "@INBOX/REMOVE_EMPTY_CHAT_CHANNEL_ITEMS", + + SAVE_LAST_STATE = "@INBOX/SAVE_LAST_STATE", } diff --git a/src/store/states/inbox/reducer.ts b/src/store/states/inbox/reducer.ts index 3f981a4ea..a0b3fac21 100644 --- a/src/store/states/inbox/reducer.ts +++ b/src/store/states/inbox/reducer.ts @@ -1,5 +1,6 @@ import produce from "immer"; import { WritableDraft } from "immer/dist/types/types-external"; +import { pick } from "lodash"; import { ActionType, createReducer } from "typesafe-actions"; import { InboxItemType, QueryParamKey } from "@/shared/constants"; import { @@ -11,7 +12,7 @@ import { ChatChannel, CommonFeed, Timestamp } from "@/shared/models"; import { areTimestampsEqual } from "@/shared/utils"; import { getQueryParam } from "@/shared/utils/queryParams"; import * as actions from "./actions"; -import { InboxItems, InboxSearchState, InboxState } from "./types"; +import { InboxItems, InboxSearchState, InboxState, LastState } from "./types"; import { getFeedLayoutItemDateForSorting } from "./utils"; type Action = ActionType; @@ -39,6 +40,8 @@ export const INITIAL_INBOX_STATE: InboxState = { sharedItem: null, chatChannelItems: [], nextChatChannelItemId: null, + lastReadState: null, + lastUnreadState: null, }; const sortInboxItems = (data: FeedLayoutItemWithFollowData[]): void => { @@ -446,12 +449,26 @@ export const reducer = createReducer(INITIAL_INBOX_STATE) return { ...INITIAL_INBOX_STATE }; }) - .handleAction(actions.getInboxItems.request, (state) => + .handleAction(actions.getInboxItems.request, (state, { payload }) => produce(state, (nextState) => { - nextState.items = { - ...nextState.items, - loading: true, - }; + const { unread = false, shouldUseLastStateIfExists = false } = payload; + const lastState = unread + ? nextState.lastUnreadState + : nextState.lastReadState; + + if (!shouldUseLastStateIfExists || !lastState) { + nextState.items = { + ...nextState.items, + loading: true, + }; + return; + } + + nextState.items = lastState.items; + nextState.sharedFeedItemId = lastState.sharedFeedItemId; + nextState.sharedItem = lastState.sharedItem; + nextState.chatChannelItems = lastState.chatChannelItems; + nextState.nextChatChannelItemId = lastState.nextChatChannelItemId; }), ) .handleAction(actions.getInboxItems.success, (state, { payload }) => @@ -727,4 +744,35 @@ export const reducer = createReducer(INITIAL_INBOX_STATE) ); } }), + ) + .handleAction(actions.saveLastState, (state, { payload }) => + produce(state, (nextState) => { + const { shouldSaveAsReadState } = payload; + const stateToSave: LastState = pick(nextState, [ + "items", + "sharedFeedItemId", + "sharedItem", + "chatChannelItems", + "nextChatChannelItemId", + ]); + const data = stateToSave.items.data || []; + stateToSave.items = { + ...stateToSave.items, + loading: false, + hasMore: true, + firstDocTimestamp: + (data[0] && getFeedLayoutItemDateForSorting(data[0])) || null, + lastDocTimestamp: + (data[data.length - 1] && + getFeedLayoutItemDateForSorting(data[data.length - 1])) || + null, + batchNumber: data.length > 15 ? stateToSave.items.batchNumber : 2, + }; + + if (shouldSaveAsReadState) { + nextState.lastReadState = stateToSave; + } else { + nextState.lastUnreadState = stateToSave; + } + }), ); diff --git a/src/store/states/inbox/saga/getInboxItems.ts b/src/store/states/inbox/saga/getInboxItems.ts index 497f2dc2c..daec01909 100644 --- a/src/store/states/inbox/saga/getInboxItems.ts +++ b/src/store/states/inbox/saga/getInboxItems.ts @@ -30,7 +30,7 @@ export function* getInboxItems( action: ReturnType, ) { const { - payload: { limit, unread = false }, + payload: { limit, unread = false, shouldUseLastStateIfExists = false }, } = action; try { @@ -42,6 +42,11 @@ export function* getInboxItems( const currentItems = (yield select(selectInboxItems)) as InboxItems; const isFirstRequest = !currentItems.lastDocTimestamp; + + if (shouldUseLastStateIfExists && !isFirstRequest) { + return; + } + const { data, firstDocTimestamp, lastDocTimestamp, hasMore } = (yield call( UserService.getInboxItemsWithMetadata, { diff --git a/src/store/states/inbox/saga/refetchInboxItems.ts b/src/store/states/inbox/saga/refetchInboxItems.ts index 1d579e36b..0eca1c871 100644 --- a/src/store/states/inbox/saga/refetchInboxItems.ts +++ b/src/store/states/inbox/saga/refetchInboxItems.ts @@ -17,11 +17,17 @@ export function* refetchInboxItems( yield put(actions.resetSearchInboxItems()); } + yield put( + actions.saveLastState({ + shouldSaveAsReadState: unread, + }), + ); yield put(actions.resetInboxItems()); yield put( actions.getInboxItems.request({ limit: 15, unread, + shouldUseLastStateIfExists: true, }), ); } diff --git a/src/store/states/inbox/types.ts b/src/store/states/inbox/types.ts index 575268674..58542ac2a 100644 --- a/src/store/states/inbox/types.ts +++ b/src/store/states/inbox/types.ts @@ -20,6 +20,15 @@ export interface InboxItems { unread: boolean; } +export type LastState = Pick< + InboxState, + | "items" + | "sharedFeedItemId" + | "sharedItem" + | "chatChannelItems" + | "nextChatChannelItemId" +>; + export interface InboxState { items: InboxItems; sharedFeedItemId: string | null; @@ -27,4 +36,6 @@ export interface InboxState { chatChannelItems: ChatChannelLayoutItem[]; nextChatChannelItemId: string | null; searchState: InboxSearchState; + lastReadState: LastState | null; + lastUnreadState: LastState | null; } diff --git a/src/store/transforms.ts b/src/store/transforms.ts index 14f18af0a..01f30a080 100644 --- a/src/store/transforms.ts +++ b/src/store/transforms.ts @@ -33,6 +33,8 @@ export const inboxTransform = createTransform( if (inboundState.items.unread) { return { ...inboundState, + lastReadState: null, + lastUnreadState: null, items: { ...INITIAL_INBOX_ITEMS }, }; } @@ -42,6 +44,8 @@ export const inboxTransform = createTransform( return { ...inboundState, + lastReadState: null, + lastUnreadState: null, items: { ...inboundState.items, data,