Skip to content

Commit

Permalink
Merge pull request #2381 from daostack/feature/CW-2317-unread-enhance…
Browse files Browse the repository at this point in the history
…ments

Inbox unread filter fixes - Real-time items update #2317
  • Loading branch information
andreymikhadyuk authored Dec 5, 2023
2 parents 8a2ba99 + c137236 commit ec3d840
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 22 deletions.
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,
);
}
73 changes: 73 additions & 0 deletions src/store/states/inbox/saga/refreshUnreadInboxItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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";

export function* refreshUnreadInboxItems() {
try {
const currentItems = (yield select(selectInboxItems)) as InboxItems;
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;
startAfter = lastDocTimestamp;
}

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

0 comments on commit ec3d840

Please sign in to comment.