Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inbox unread filter fixes - Real-time items update #2317 #2381

Merged
merged 8 commits into from
Dec 5, 2023
4 changes: 4 additions & 0 deletions src/pages/inbox/BaseInbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ const InboxPage: FC<InboxPageProps> = (props) => {

useEffect(() => {
fetchData();

return () => {
dispatch(inboxActions.resetInbox({ onlyIfUnread: true }));
};
}, [userId]);

useEffect(() => {
Expand Down
23 changes: 23 additions & 0 deletions src/shared/converters/ChatChannelToLayoutItemConverter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { InboxItemType } from "@/shared/constants";
import { ChatChannelLayoutItem } from "@/shared/interfaces";
import { ChatChannel } from "@/shared/models";
import { Converter } from "./Converter";

class ChatChannelToLayoutItemConverter extends Converter<
ChatChannel,
ChatChannelLayoutItem
> {
public toTargetEntity = (
chatChannel: ChatChannel,
): ChatChannelLayoutItem => ({
type: InboxItemType.ChatChannel,
itemId: chatChannel.id,
chatChannel,
});

public toBaseEntity = (
chatChannelLayoutItem: ChatChannelLayoutItem,
): ChatChannel => chatChannelLayoutItem.chatChannel;
}

export default new ChatChannelToLayoutItemConverter();
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { InboxItemType } from "@/shared/constants";
import { FeedItemFollowLayoutItemWithFollowData } from "@/shared/interfaces";
import { FeedItemFollowWithMetadata } from "@/shared/models";
import { Converter } from "./Converter";

class FeedItemFollowToLayoutItemWithFollowDataConverter extends Converter<
FeedItemFollowWithMetadata,
FeedItemFollowLayoutItemWithFollowData
> {
public toTargetEntity = (
feedItemFollowWithMetadata: FeedItemFollowWithMetadata,
): FeedItemFollowLayoutItemWithFollowData => ({
type: InboxItemType.FeedItemFollow,
itemId: feedItemFollowWithMetadata.feedItemId,
feedItem: feedItemFollowWithMetadata.feedItem,
feedItemFollowWithMetadata: feedItemFollowWithMetadata,
});

public toBaseEntity = (
feedItemFollowLayoutItemWithFollowData: FeedItemFollowLayoutItemWithFollowData,
): FeedItemFollowWithMetadata =>
feedItemFollowLayoutItemWithFollowData.feedItemFollowWithMetadata;
}

export default new FeedItemFollowToLayoutItemWithFollowDataConverter();
2 changes: 2 additions & 0 deletions src/shared/converters/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { default as ChatChannelToDiscussionConverter } from "./ChatChannelToDiscussionConverter";
export { default as ChatChannelToLayoutItemConverter } from "./ChatChannelToLayoutItemConverter";
export { default as ChatMessageToUserDiscussionMessageConverter } from "./ChatMessageToUserDiscussionMessageConverter";
export { default as FeedItemFollowToLayoutItemWithFollowDataConverter } from "./FeedItemFollowToLayoutItemWithFollowDataConverter";
2 changes: 2 additions & 0 deletions src/shared/hooks/useCases/useInboxItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
FeedItemFollowWithMetadata,
} from "@/shared/models";
import { inboxActions, InboxItems, selectInboxItems } from "@/store/states";
import { useUnreadInboxItems } from "./useUnreadInboxItems";

interface Return
extends Pick<InboxItems, "data" | "loading" | "hasMore" | "batchNumber"> {
Expand Down Expand Up @@ -128,6 +129,7 @@ export const useInboxItems = (
const userId = user?.uid;
const unread = options?.unread;
const lastBatch = newItemsBatches[0];
useUnreadInboxItems(options?.unread);

const fetch = () => {
dispatch(
Expand Down
36 changes: 36 additions & 0 deletions src/shared/hooks/useCases/useUnreadInboxItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { usePreviousDistinct, useUpdateEffect } from "react-use";
import { selectUserStreamsWithNotificationsAmount } from "@/pages/Auth/store/selectors";
import { inboxActions } from "@/store/states";

export const useUnreadInboxItems = (unread?: boolean): void => {
const dispatch = useDispatch();
const notificationsAmount = useSelector(
selectUserStreamsWithNotificationsAmount(),
);
const previousNotificationsAmount = usePreviousDistinct(notificationsAmount);

useUpdateEffect(() => {
if (
!unread ||
!notificationsAmount ||
(typeof previousNotificationsAmount === "number" &&
notificationsAmount < previousNotificationsAmount)
) {
return;
}

dispatch(inboxActions.refreshUnreadInboxItems.request());
}, [notificationsAmount]);

useEffect(() => {
return () => {
dispatch(
inboxActions.refreshUnreadInboxItems.cancel(
"Cancel unread inbox items refresh on unmount",
),
);
};
}, []);
};
11 changes: 10 additions & 1 deletion src/store/states/inbox/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { ChatChannel, CommonFeed } from "@/shared/models";
import { InboxActionType } from "./constants";
import { InboxItems } from "./types";

export const resetInbox = createStandardAction(InboxActionType.RESET_INBOX)();
export const resetInbox = createStandardAction(InboxActionType.RESET_INBOX)<{
onlyIfUnread?: boolean;
} | void>();

export const getInboxItems = createAsyncAction(
InboxActionType.GET_INBOX_ITEMS,
Expand All @@ -21,6 +23,13 @@ export const getInboxItems = createAsyncAction(
string
>();

export const refreshUnreadInboxItems = createAsyncAction(
InboxActionType.REFRESH_UNREAD_INBOX_ITEMS,
InboxActionType.REFRESH_UNREAD_INBOX_ITEMS_SUCCESS,
InboxActionType.REFRESH_UNREAD_INBOX_ITEMS_FAILURE,
InboxActionType.REFRESH_UNREAD_INBOX_ITEMS_CANCEL,
)<void, void, void, string>();

export const addNewInboxItems = createStandardAction(
InboxActionType.ADD_NEW_INBOX_ITEMS,
)<
Expand Down
5 changes: 5 additions & 0 deletions src/store/states/inbox/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export enum InboxActionType {
GET_INBOX_ITEMS_FAILURE = "@INBOX/GET_INBOX_ITEMS_FAILURE",
GET_INBOX_ITEMS_CANCEL = "@INBOX/GET_INBOX_ITEMS_CANCEL",

REFRESH_UNREAD_INBOX_ITEMS = "@INBOX/REFRESH_UNREAD_INBOX_ITEMS",
REFRESH_UNREAD_INBOX_ITEMS_SUCCESS = "@INBOX/REFRESH_UNREAD_INBOX_ITEMS_SUCCESS",
REFRESH_UNREAD_INBOX_ITEMS_FAILURE = "@INBOX/REFRESH_UNREAD_INBOX_ITEMS_FAILURE",
REFRESH_UNREAD_INBOX_ITEMS_CANCEL = "@INBOX/REFRESH_UNREAD_INBOX_ITEMS_CANCEL",

ADD_NEW_INBOX_ITEMS = "@INBOX/ADD_NEW_INBOX_ITEMS",

UPDATE_INBOX_ITEM = "@INBOX/UPDATE_INBOX_ITEM",
Expand Down
8 changes: 7 additions & 1 deletion src/store/states/inbox/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,13 @@ const updateChatChannelItem = (
};

export const reducer = createReducer<InboxState, Action>(INITIAL_INBOX_STATE)
.handleAction(actions.resetInbox, () => ({ ...INITIAL_INBOX_STATE }))
.handleAction(actions.resetInbox, (state, { payload }) => {
if (payload?.onlyIfUnread && !state.items.unread) {
return state;
}

return { ...INITIAL_INBOX_STATE };
})
.handleAction(actions.getInboxItems.request, (state) =>
produce(state, (nextState) => {
nextState.items = {
Expand Down
28 changes: 8 additions & 20 deletions src/store/states/inbox/saga/getInboxItems.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { call, put, select } from "redux-saga/effects";
import { UserService } from "@/services";
import { InboxItemType } from "@/shared/constants";
import {
Awaited,
ChatChannelLayoutItem,
FeedItemFollowLayoutItemWithFollowData,
FeedLayoutItemWithFollowData,
} from "@/shared/interfaces";
ChatChannelToLayoutItemConverter,
FeedItemFollowToLayoutItemWithFollowDataConverter,
} from "@/shared/converters";
import { Awaited, FeedLayoutItemWithFollowData } from "@/shared/interfaces";
import { isError } from "@/shared/utils";
import * as actions from "../actions";
import { selectInboxItems } from "../selectors";
Expand Down Expand Up @@ -44,21 +42,11 @@ export function* getInboxItems(
},
)) as Awaited<ReturnType<typeof UserService.getInboxItems>>;
const chatChannelItems = data.chatChannels
.map<ChatChannelLayoutItem>((chatChannel) => ({
type: InboxItemType.ChatChannel,
itemId: chatChannel.id,
chatChannel,
}))
.map((item) => ChatChannelToLayoutItemConverter.toTargetEntity(item))
.filter((item) => item.chatChannel.messageCount > 0);
const feedItemFollowItems =
data.feedItemFollows.map<FeedItemFollowLayoutItemWithFollowData>(
(feedItemFollowWithMetadata) => ({
type: InboxItemType.FeedItemFollow,
itemId: feedItemFollowWithMetadata.feedItemId,
feedItem: feedItemFollowWithMetadata.feedItem,
feedItemFollowWithMetadata: feedItemFollowWithMetadata,
}),
);
const feedItemFollowItems = data.feedItemFollows.map((item) =>
FeedItemFollowToLayoutItemWithFollowDataConverter.toTargetEntity(item),
);
const convertedData = sortItems([
...chatChannelItems,
...feedItemFollowItems,
Expand Down
6 changes: 6 additions & 0 deletions src/store/states/inbox/saga/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { takeLatestWithCancel } from "@/shared/utils/saga";
import * as actions from "../actions";
import { getInboxItems } from "./getInboxItems";
import { refreshUnreadInboxItems } from "./refreshUnreadInboxItems";

export function* mainSaga() {
yield takeLatestWithCancel(
actions.getInboxItems.request,
actions.getInboxItems.cancel,
getInboxItems,
);
yield takeLatestWithCancel(
actions.refreshUnreadInboxItems.request,
actions.refreshUnreadInboxItems.cancel,
refreshUnreadInboxItems,
);
}
90 changes: 90 additions & 0 deletions src/store/states/inbox/saga/refreshUnreadInboxItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { call, put, select } from "redux-saga/effects";
import { Logger, UserService } from "@/services";
import {
ChatChannelToLayoutItemConverter,
FeedItemFollowToLayoutItemWithFollowDataConverter,
} from "@/shared/converters";
import { Awaited, FeedLayoutItemWithFollowData } from "@/shared/interfaces";
import { Timestamp } from "@/shared/models";
import * as actions from "../actions";
import { selectInboxItems } from "../selectors";
import { InboxItems } from "../types";

const checkCanKeepFetchingByDate = (
firstDocTimestamp: Timestamp | null,
lastDocTimestamp: Timestamp | null,
): boolean => {
if (!firstDocTimestamp) {
return true;
}
if (!lastDocTimestamp) {
return false;
}

return lastDocTimestamp.seconds >= firstDocTimestamp.seconds;
};

export function* refreshUnreadInboxItems() {
try {
const currentItems = (yield select(selectInboxItems)) as InboxItems;
const { firstDocTimestamp } = currentItems;
const newInboxItems: FeedLayoutItemWithFollowData[] = [];
let startAfter: Timestamp | null = null;
let keepItemsFetching = true;

while (keepItemsFetching) {
const { data, lastDocTimestamp, hasMore } = (yield call(
UserService.getInboxItems,
{
startAfter,
limit: 5,
unread: true,
},
)) as Awaited<ReturnType<typeof UserService.getInboxItems>>;
const chatChannelItems = data.chatChannels
.filter(
(chatChannel) =>
chatChannel.messageCount > 0 &&
(!currentItems.data ||
currentItems.data.every(
(item) => item.itemId !== chatChannel.id,
)),
)
.map((item) => ChatChannelToLayoutItemConverter.toTargetEntity(item));
const feedItemFollowItems = data.feedItemFollows
.filter(
(feedItemFollow) =>
!currentItems.data ||
currentItems.data.every(
(item) => item.itemId !== feedItemFollow.id,
),
)
.map((item) =>
FeedItemFollowToLayoutItemWithFollowDataConverter.toTargetEntity(
item,
),
);
newInboxItems.push(...chatChannelItems, ...feedItemFollowItems);
keepItemsFetching =
hasMore &&
checkCanKeepFetchingByDate(firstDocTimestamp, lastDocTimestamp);
startAfter = lastDocTimestamp;
}

if (newInboxItems.length > 0) {
yield put(
actions.addNewInboxItems(
newInboxItems.map((item) => ({
item,
statuses: {
isAdded: false,
isRemoved: false,
},
})),
),
);
}
} catch (err) {
Logger.error(err);
}
}
Loading