diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af219892..345a252cc7 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" +# #!/bin/sh +# . "$(dirname "$0")/_/husky.sh" -npx lint-staged +# npx lint-staged diff --git a/src/pages/Auth/store/saga.tsx b/src/pages/Auth/store/saga.tsx index a3240b05d9..a73fb31f78 100755 --- a/src/pages/Auth/store/saga.tsx +++ b/src/pages/Auth/store/saga.tsx @@ -6,7 +6,7 @@ import { subscribeToNotification, } from "@/pages/OldCommon/store/api"; import { UserService } from "@/services"; -import { store } from "@/shared/appConfig"; +import { persistor, store } from "@/shared/appConfig"; import { Awaited } from "@/shared/interfaces"; import { FirebaseCredentials } from "@/shared/interfaces/FirebaseCredentials"; import { EventTypeState, NotificationItem } from "@/shared/models/Notification"; @@ -48,6 +48,7 @@ import firebase from "../../../shared/utils/firebase"; import { UserCreationDto } from "../interface"; import * as actions from "./actions"; import { createdUserApi, deleteUserApi, getUserData } from "./api"; +import { resetOptimisticState } from "@/store/states/optimistic/actions"; const getAuthProviderFromProviderData = ( providerData?: firebase.User["providerData"], @@ -533,15 +534,27 @@ function* confirmVerificationCodeSaga({ } function* logOut() { + + yield put(resetOptimisticState()); + // Wait for persistor.purge() to complete + yield call([persistor, persistor.purge]); + yield call([persistor, persistor.flush]); + + // Now clear localStorage localStorage.clear(); - firebase.auth().signOut(); + // Sign out from Firebase + yield call([firebase.auth(), firebase.auth().signOut]); + + // Notify React Native WebView if applicable if (window.ReactNativeWebView) { - window?.ReactNativeWebView?.postMessage(WebviewActions.logout); + window.ReactNativeWebView.postMessage(WebviewActions.logout); } + // Reset global data and navigate to home resetGlobalData(true); history.push(ROUTE_PATHS.HOME); + yield true; } diff --git a/src/pages/OldCommon/components/CommonDetailContainer/WalletComponent/hooks.ts b/src/pages/OldCommon/components/CommonDetailContainer/WalletComponent/hooks.ts index a6de0ef00a..5d22347388 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/WalletComponent/hooks.ts +++ b/src/pages/OldCommon/components/CommonDetailContainer/WalletComponent/hooks.ts @@ -25,116 +25,116 @@ export const useCommonTransactionsChartDataSet = orderedCommonTransactions: TransactionData[], commonCreatedAt?: Time, ) => { - const uniqueTransactionsMonths = new Set(); - - const groupedByMonthPayInsSummaries: { [key: string]: number } = {}; - const groupedByMonthPayOutsSummaries: { [key: string]: number } = {}; - - orderedCommonTransactions - .filter( - (transaction) => - getMonthsDifference( - new Date(transaction.createdAt.seconds * 1000), - new Date(), - ) <= TRANSACTIONS_PERIOD_MONTHS_AMOUNT, - ) - .map((transaction) => ({ - ...transaction, - amount: transaction.amount / 100, - })) - .reverse() - .map((transaction) => { - const transactionMonthNotation = - BRIEF_MONTH_NAMES[ - new Date(transaction.createdAt.seconds * 1000).getMonth() - ]; - - uniqueTransactionsMonths.add(transactionMonthNotation); - - if ( - groupedByMonthPayInsSummaries[transactionMonthNotation] === - undefined - ) - groupedByMonthPayInsSummaries[transactionMonthNotation] = 0; - - if ( - groupedByMonthPayOutsSummaries[transactionMonthNotation] === - undefined - ) - groupedByMonthPayOutsSummaries[transactionMonthNotation] = 0; - - if (transaction.type === TransactionType.PayIn) { - groupedByMonthPayInsSummaries[transactionMonthNotation] += - transaction.amount; - } else if (transaction.type === TransactionType.PayOut) { - groupedByMonthPayOutsSummaries[transactionMonthNotation] += - transaction.amount; - } - - return transaction; - }); - - const chartMonthLabelsList = Array.from( - uniqueTransactionsMonths, - ) as string[]; - - /* - FIXME: tempo decision to prevent common's crashing (some common-records have createdAt set in null), - should be reverted after full merging of the Governance & clearing the DB from legacy stuff - */ - if (commonCreatedAt) { - const commonCreatedAtMonthNotation = - BRIEF_MONTH_NAMES[ - new Date(commonCreatedAt.seconds * 1000).getMonth() - ]; - - if ( - !chartMonthLabelsList.includes(commonCreatedAtMonthNotation) && - getMonthsDifference( - new Date(commonCreatedAt.seconds * 1000), - new Date(), - ) <= TRANSACTIONS_PERIOD_MONTHS_AMOUNT - ) { - chartMonthLabelsList.unshift(commonCreatedAtMonthNotation); - - groupedByMonthPayInsSummaries[commonCreatedAtMonthNotation] = 0; - groupedByMonthPayOutsSummaries[commonCreatedAtMonthNotation] = 0; - } - } - - const payInsChartData = chartMonthLabelsList.map( - (monthLabel) => groupedByMonthPayInsSummaries[monthLabel], - ); - const payOutsChartData = chartMonthLabelsList.map( - (monthLabel) => groupedByMonthPayOutsSummaries[monthLabel], - ); - const balanceChartData = payInsChartData.reduce( - ( - accum: { currentBalance: number; balances: number[] }, - payInsMonthSum, - index, - ) => { - let newBalance = accum.currentBalance; - - newBalance += payInsMonthSum; - newBalance -= payOutsChartData[index]; - - return { - currentBalance: newBalance, - balances: [...accum.balances, newBalance], - }; - }, - { - currentBalance: 0, - balances: [], - }, - ).balances; + // const uniqueTransactionsMonths = new Set(); + + // const groupedByMonthPayInsSummaries: { [key: string]: number } = {}; + // const groupedByMonthPayOutsSummaries: { [key: string]: number } = {}; + + // orderedCommonTransactions + // .filter( + // (transaction) => + // getMonthsDifference( + // new Date(transaction.createdAt.seconds * 1000), + // new Date(), + // ) <= TRANSACTIONS_PERIOD_MONTHS_AMOUNT, + // ) + // .map((transaction) => ({ + // ...transaction, + // amount: transaction.amount / 100, + // })) + // .reverse() + // .map((transaction) => { + // const transactionMonthNotation = + // BRIEF_MONTH_NAMES[ + // new Date(transaction.createdAt.seconds * 1000).getMonth() + // ]; + + // uniqueTransactionsMonths.add(transactionMonthNotation); + + // if ( + // groupedByMonthPayInsSummaries[transactionMonthNotation] === + // undefined + // ) + // groupedByMonthPayInsSummaries[transactionMonthNotation] = 0; + + // if ( + // groupedByMonthPayOutsSummaries[transactionMonthNotation] === + // undefined + // ) + // groupedByMonthPayOutsSummaries[transactionMonthNotation] = 0; + + // if (transaction.type === TransactionType.PayIn) { + // groupedByMonthPayInsSummaries[transactionMonthNotation] += + // transaction.amount; + // } else if (transaction.type === TransactionType.PayOut) { + // groupedByMonthPayOutsSummaries[transactionMonthNotation] += + // transaction.amount; + // } + + // return transaction; + // }); + + // const chartMonthLabelsList = Array.from( + // uniqueTransactionsMonths, + // ) as string[]; + + // /* + // FIXME: tempo decision to prevent common's crashing (some common-records have createdAt set in null), + // should be reverted after full merging of the Governance & clearing the DB from legacy stuff + // */ + // if (commonCreatedAt) { + // const commonCreatedAtMonthNotation = + // BRIEF_MONTH_NAMES[ + // new Date(commonCreatedAt.seconds * 1000).getMonth() + // ]; + + // if ( + // !chartMonthLabelsList.includes(commonCreatedAtMonthNotation) && + // getMonthsDifference( + // new Date(commonCreatedAt.seconds * 1000), + // new Date(), + // ) <= TRANSACTIONS_PERIOD_MONTHS_AMOUNT + // ) { + // chartMonthLabelsList.unshift(commonCreatedAtMonthNotation); + + // groupedByMonthPayInsSummaries[commonCreatedAtMonthNotation] = 0; + // groupedByMonthPayOutsSummaries[commonCreatedAtMonthNotation] = 0; + // } + // } + + // const payInsChartData = chartMonthLabelsList.map( + // (monthLabel) => groupedByMonthPayInsSummaries[monthLabel], + // ); + // const payOutsChartData = chartMonthLabelsList.map( + // (monthLabel) => groupedByMonthPayOutsSummaries[monthLabel], + // ); + // const balanceChartData = payInsChartData.reduce( + // ( + // accum: { currentBalance: number; balances: number[] }, + // payInsMonthSum, + // index, + // ) => { + // let newBalance = accum.currentBalance; + + // newBalance += payInsMonthSum; + // newBalance -= payOutsChartData[index]; + + // return { + // currentBalance: newBalance, + // balances: [...accum.balances, newBalance], + // }; + // }, + // { + // currentBalance: 0, + // balances: [], + // }, + // ).balances; return { - chartMonthLabelsList, - payInsChartData, - payOutsChartData, - balanceChartData, + chartMonthLabelsList: [], + payInsChartData: [], + payOutsChartData: [], + balanceChartData: [], }; }, [], diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 5bd1d392da..c7350bd0b1 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -65,6 +65,9 @@ import { selectOptimisticFeedItems, commonActions, selectOptimisticDiscussionMessages, + inboxActions, + optimisticActions, + selectInstantDiscussionMessagesOrder, } from "@/store/states"; import { ChatContentContext, ChatContentData } from "../CommonContent/context"; import { @@ -85,8 +88,10 @@ import { } from "./utils"; import styles from "./ChatComponent.module.scss"; import { BaseTextEditorHandles } from "@/shared/ui-kit/TextEditor/BaseTextEditor"; +import { useFeedItemContext } from "../FeedItem"; const BASE_CHAT_INPUT_HEIGHT = 48; +const BASE_ORDER_INTERVAL = 1000; interface ChatComponentInterface { commonId: string; @@ -254,6 +259,20 @@ export default function ChatComponent({ parseStringToTextEditorValue(), ); + const { + setIsInputFocused + } = useFeedItemContext(); + + useEffect(() => { + const isEmpty = checkIsTextEditorValueEmpty(message); + if(!isEmpty || message.length > 1) { + setIsInputFocused?.(true); + } else { + setIsInputFocused?.(false); + } + + },[message, setIsInputFocused]) + const emojiCount = useMemo( () => countTextEditorEmojiElements(message), [message], @@ -275,6 +294,9 @@ export default function ChatComponent({ const optimisticDiscussionMessages = useSelector( selectOptimisticDiscussionMessages, ); + const instantDiscussionMessagesOrder = useSelector(selectInstantDiscussionMessagesOrder); + + const currentChatOrder = instantDiscussionMessagesOrder.get(discussionId)?.order || 1; const isOptimisticChat = optimisticFeedItems.has(discussionId); @@ -295,7 +317,7 @@ export default function ChatComponent({ }); dispatch( - commonActions.clearOptimisticDiscussionMessages( + optimisticActions.clearOptimisticDiscussionMessages( optimisticMessageDiscussionId, ), ); @@ -414,8 +436,8 @@ export default function ChatComponent({ setMessages([]); } }, - 1500, - [newMessages, discussionId, dispatch], + 1500 + BASE_ORDER_INTERVAL * currentChatOrder, + [newMessages, discussionId, dispatch, currentChatOrder], ); /** @@ -573,7 +595,7 @@ export default function ChatComponent({ } if (isOptimisticChat) { - dispatch(commonActions.setOptimisticDiscussionMessages(payload)); + dispatch(optimisticActions.setOptimisticDiscussionMessages(payload)); } else { setMessages((prev) => { if (isFilesMessageWithoutTextAndImages) { @@ -582,6 +604,7 @@ export default function ChatComponent({ return [...prev, ...filePreviewPayload, payload]; }); + dispatch(optimisticActions.setInstantDiscussionMessagesOrder({discussionId})); } if (isChatChannel) { @@ -606,6 +629,23 @@ export default function ChatComponent({ if (currentFilesPreview) { dispatch(chatActions.clearFilesPreview()); } + + const payloadUpdateFeedItem = { + feedItemId, + lastMessage: { + messageId: pendingMessageId, + ownerId: userId as string, + userName: getUserName(user), + ownerType: DiscussionMessageOwnerType.User, + content: JSON.stringify(message), + } + }; + + dispatch(commonActions.setFeedItemUpdatedAt(payloadUpdateFeedItem)); + dispatch(inboxActions.setInboxItemUpdatedAt(payloadUpdateFeedItem)); + document + .getElementById("feedLayoutWrapper") + ?.scrollIntoView({ behavior: "smooth" }); focusOnChat(); } }, @@ -620,6 +660,7 @@ export default function ChatComponent({ isChatChannel, linkPreviewData, isOptimisticChat, + feedItemId, ], ); diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx index e30f9ff74c..d70e1ee08b 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx @@ -20,6 +20,7 @@ import { } from "@/shared/ui-kit/TextEditor"; import { generateFirstMessage, generateOptimisticFeedItem, getUserName } from "@/shared/utils"; import { + optimisticActions, selectDiscussionCreationData, selectIsDiscussionCreationLoading, } from "@/store/states"; @@ -122,26 +123,27 @@ const NewDiscussionCreation: FC = (props) => { } else { const discussionId = uuidv4(); const userName = getUserName(user); - dispatch( - commonActions.setOptimisticFeedItem( - generateOptimisticFeedItem({ - userId, - commonId: common.id, - type: CommonFeedType.OptimisticDiscussion, - circleVisibility, - discussionId, - title: values.title, - content: JSON.stringify(values.content), - lastMessageContent: { - ownerId: userId, - userName, - ownerType: DiscussionMessageOwnerType.System, - content: generateFirstMessage({userName, userId}), - } - }), - ), - ); + const optimisticFeedItem = generateOptimisticFeedItem({ + userId, + commonId: common.id, + type: CommonFeedType.OptimisticDiscussion, + circleVisibility, + discussionId, + title: values.title, + content: JSON.stringify(values.content), + lastMessageContent: { + ownerId: userId, + userName, + ownerType: DiscussionMessageOwnerType.System, + content: generateFirstMessage({userName, userId}), + } + }); + dispatch(optimisticActions.setOptimisticFeedItem({ + data: optimisticFeedItem, + common + })); + dispatch(commonActions.setRecentStreamId(optimisticFeedItem.data.id)); dispatch( commonActions.createDiscussion.request({ payload: { diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx index ce18e3c255..06ba04f3a6 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/NewProposalCreation.tsx @@ -18,6 +18,7 @@ import { import { parseStringToTextEditorValue } from "@/shared/ui-kit/TextEditor"; import { generateFirstMessage, generateOptimisticFeedItem, getUserName } from "@/shared/utils"; import { + optimisticActions, selectIsProposalCreationLoading, selectProposalCreationData, } from "@/store/states"; @@ -90,25 +91,27 @@ const NewProposalCreation: FC = (props) => { const discussionId = uuidv4(); const userName = getUserName(user); - dispatch( - commonActions.setOptimisticFeedItem( - generateOptimisticFeedItem({ - userId, - commonId: common.id, - type: CommonFeedType.OptimisticProposal, - circleVisibility: userCircleIds, - discussionId, - title: values.title, - content: JSON.stringify(values.content), - lastMessageContent: { - ownerId: userId, - userName, - ownerType: DiscussionMessageOwnerType.System, - content: generateFirstMessage({userName, userId}), - } - }), - ), - ); + const optimisticFeedItem = generateOptimisticFeedItem({ + userId, + commonId: common.id, + type: CommonFeedType.OptimisticProposal, + circleVisibility: userCircleIds, + discussionId, + title: values.title, + content: JSON.stringify(values.content), + lastMessageContent: { + ownerId: userId, + userName, + ownerType: DiscussionMessageOwnerType.System, + content: generateFirstMessage({userName, userId}), + } + }); + + dispatch(optimisticActions.setOptimisticFeedItem({ + data: optimisticFeedItem, + common + })); + dispatch(commonActions.setRecentStreamId(optimisticFeedItem.data.id)); switch (values.proposalType.value) { case ProposalsTypes.FUNDS_ALLOCATION: { const fundingProposalPayload = getFundingProposalPayload( diff --git a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx index ed66caaa2b..dcd82a373c 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -49,7 +49,7 @@ import { DiscussionFeedCardContent, } from "./components"; import { useMenuItems } from "./hooks"; -import { commonActions } from "@/store/states"; +import { optimisticActions } from "@/store/states"; interface DiscussionFeedCardProps { item: CommonFeed; @@ -218,12 +218,6 @@ function DiscussionFeedCard(props, ref) { const cardTitle = discussion?.title; const commonNotion = outerCommonNotion ?? common?.notion; - // const ownerId = useMemo(() => { - // if(item.userId) { - // return item.userId - // } - // },[item.userId]) - const handleOpenChat = useCallback(() => { if (discussion && !isPreviewMode) { setChatItem({ @@ -288,7 +282,7 @@ function DiscussionFeedCard(props, ref) { useEffect(() => { if(item.data.lastMessage?.content && discussion?.id && isOptimisticallyCreated) { markFeedItemAsSeen({feedObjectId: item.id, commonId}) - setTimeout(() => dispatch(commonActions.clearCreatedOptimisticFeedItem(discussion?.id)), 10000); + setTimeout(() => dispatch(optimisticActions.clearCreatedOptimisticFeedItem(discussion?.id)), 10000); } },[item.id, item.data.lastMessage?.content, discussion?.id, isOptimisticallyCreated, commonId]) diff --git a/src/pages/common/components/FeedCard/FeedCard.module.scss b/src/pages/common/components/FeedCard/FeedCard.module.scss index 7deb66dbe2..8ff002e12a 100644 --- a/src/pages/common/components/FeedCard/FeedCard.module.scss +++ b/src/pages/common/components/FeedCard/FeedCard.module.scss @@ -23,6 +23,10 @@ cursor: pointer; } +.toggleCard { + outline: none; +} + .loader { align-self: center; } diff --git a/src/pages/common/components/FeedCard/FeedCard.tsx b/src/pages/common/components/FeedCard/FeedCard.tsx index 28860dc8a3..5fac7dcb50 100644 --- a/src/pages/common/components/FeedCard/FeedCard.tsx +++ b/src/pages/common/components/FeedCard/FeedCard.tsx @@ -300,7 +300,7 @@ const FeedCard = (props, ref) => { return (
- {!isPreviewMode &&
{feedItemBaseContent}
} + {!isPreviewMode &&
{feedItemBaseContent}
}
void; setExpandedFeedItemId?: (feedItemId: string | null) => void; renderFeedItemBaseContent?: (props: FeedItemBaseContentProps) => ReactNode; onFeedItemUpdate?: (item: CommonFeed, isRemoved: boolean) => void; diff --git a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx index 64ba269785..91f55aafef 100644 --- a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx +++ b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx @@ -13,7 +13,7 @@ import React, { import { useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; import PullToRefresh from "react-simple-pull-to-refresh"; -import { useDeepCompareEffect, useWindowSize } from "react-use"; +import { useDeepCompareEffect, useKey, useWindowSize } from "react-use"; import classNames from "classnames"; import { selectUser } from "@/pages/Auth/store/selectors"; import { useCommonMember } from "@/pages/OldCommon/hooks"; @@ -44,7 +44,7 @@ import { ROUTE_PATHS, } from "@/shared/constants"; import { useRoutesContext } from "@/shared/contexts"; -import { useMemoizedFunction, useQueryParams } from "@/shared/hooks"; +import { useElementPresence, useMemoizedFunction, useQueryParams } from "@/shared/hooks"; import { useGovernanceByCommonId } from "@/shared/hooks/useCases"; import { useDisableOverscroll } from "@/shared/hooks/useDisableOverscroll"; import { useIsTabletView } from "@/shared/hooks/viewport"; @@ -90,6 +90,7 @@ import { import { BATCHES_AMOUNT_TO_PRELOAD, ITEMS_AMOUNT_TO_PRE_LOAD_MESSAGES, + MENTION_TAG_ELEMENT, } from "./constants"; import { useUserForProfile } from "./hooks"; import { @@ -353,6 +354,8 @@ const FeedLayout: ForwardRefRenderFunction = ( const activeFeedItemId = chatItem?.feedItemId || feedItemIdForAutoChatOpen; const sizeKey = `${windowWidth}_${contentWidth}`; + const activeFeedItemIndex = useMemo(() => allFeedItems.findIndex((item) => item.itemId === activeFeedItemId), [activeFeedItemId, allFeedItems]); + const getUserCircleIds = useCallback( (commonId) => { return Object.values( @@ -405,6 +408,50 @@ const FeedLayout: ForwardRefRenderFunction = ( setChatItem(nextChatItem); }, []); + const isMentionOpen = useElementPresence(MENTION_TAG_ELEMENT.key, MENTION_TAG_ELEMENT.value); + const [isInputFocused, setIsInputFocused] = useState(false); + + const handleArrowUp = (event, activeFeedItemIndex, allFeedItems, isMentionOpen, setChatItem) => { + if (!isMentionOpen && !isInputFocused) { + event.preventDefault(); + event.stopPropagation(); + + if (activeFeedItemIndex > 0) { + const nextFeedItemId = allFeedItems[activeFeedItemIndex - 1]?.itemId; + + setChatItem({ feedItemId: nextFeedItemId }); + } + } + }; + + const handleArrowDown = (event, activeFeedItemIndex, allFeedItems, isMentionOpen, setChatItem) => { + if (!isMentionOpen && !isInputFocused) { + event.preventDefault(); + event.stopPropagation(); + + if (activeFeedItemIndex < allFeedItems.length - 1) { + const nextFeedItemId = allFeedItems[activeFeedItemIndex + 1]?.itemId; + + setChatItem({ feedItemId: nextFeedItemId }); + } + } + }; + + // Inside your component + useKey( + "ArrowUp", + (event) => handleArrowUp(event, activeFeedItemIndex, allFeedItems, isMentionOpen, setChatItem), + {}, + [activeFeedItemIndex, allFeedItems, isMentionOpen, isInputFocused] + ); + + useKey( + "ArrowDown", + (event) => handleArrowDown(event, activeFeedItemIndex, allFeedItems, isMentionOpen, setChatItem), + {}, + [activeFeedItemIndex, allFeedItems, isMentionOpen, isInputFocused] + ); + const chatContextValue = useMemo( () => ({ setChatItem: setActiveChatItem, @@ -644,6 +691,7 @@ const FeedLayout: ForwardRefRenderFunction = ( // so we will not have extra re-renders of ALL rendered items const feedItemContextValue = useMemo( () => ({ + setIsInputFocused, setExpandedFeedItemId, renderFeedItemBaseContent, onFeedItemUpdate, @@ -656,6 +704,7 @@ const FeedLayout: ForwardRefRenderFunction = ( onActiveItemDataChange: handleActiveFeedItemDataChange, }), [ + setIsInputFocused, renderFeedItemBaseContent, onFeedItemUpdate, onFeedItemUnfollowed, @@ -852,6 +901,7 @@ const FeedLayout: ForwardRefRenderFunction = ( item.feedItemFollowWithMetadata, outerCommon, ); + const isPinned = ( outerCommon?.pinnedFeedItems || [] ).some( diff --git a/src/pages/commonFeed/components/FeedLayout/constants/index.ts b/src/pages/commonFeed/components/FeedLayout/constants/index.ts index 68c376375c..b64b9cd957 100644 --- a/src/pages/commonFeed/components/FeedLayout/constants/index.ts +++ b/src/pages/commonFeed/components/FeedLayout/constants/index.ts @@ -1,2 +1,6 @@ export const BATCHES_AMOUNT_TO_PRELOAD = 2; export const ITEMS_AMOUNT_TO_PRE_LOAD_MESSAGES = 10; +export const MENTION_TAG_ELEMENT = { + key: "data-cy", + value: "mentions-portal" +} diff --git a/src/pages/inbox/BaseInbox.tsx b/src/pages/inbox/BaseInbox.tsx index 6b34a11d8c..711ba2868d 100644 --- a/src/pages/inbox/BaseInbox.tsx +++ b/src/pages/inbox/BaseInbox.tsx @@ -27,6 +27,7 @@ import { RightArrowThinIcon } from "@/shared/icons"; import { ChatChannelFeedLayoutItemProps, checkIsChatChannelLayoutItem, + FeedItemFollowLayoutItemWithFollowData, FeedLayoutItem, FeedLayoutItemChangeDataWithType, FeedLayoutRef, @@ -37,10 +38,12 @@ import { Loader, NotFound, PureCommonTopNavigation } from "@/shared/ui-kit"; import { inboxActions, selectChatChannelItems, + selectOptimisticInboxFeedItems, selectInboxSearchValue, selectIsSearchingInboxItems, selectNextChatChannelItemId, selectSharedInboxItem, + selectInstantDiscussionMessagesOrder, } from "@/store/states"; import { ChatChannelItem, FeedItemBaseContent } from "./components"; import { useInboxData } from "./hooks"; @@ -102,9 +105,12 @@ const InboxPage: FC = (props) => { } = useInboxItems(feedItemIdsForNotListening, { unread: isActiveUnreadInboxItemsQueryParam, }); + const sharedInboxItem = useSelector(selectSharedInboxItem); const chatChannelItems = useSelector(selectChatChannelItems); const nextChatChannelItemId = useSelector(selectNextChatChannelItemId); + const optimisticInboxFeedItems = useSelector(selectOptimisticInboxFeedItems); + const getEmptyText = (): string => { if (hasMoreInboxItems) { @@ -123,6 +129,11 @@ const InboxPage: FC = (props) => { const topFeedItems = useMemo(() => { const items: FeedLayoutItem[] = []; + if (optimisticInboxFeedItems.size > 0) { + const optimisticItems = Array.from(optimisticInboxFeedItems.values()); + items.push(...optimisticItems); + } + if (chatChannelItems.length > 0) { items.push(...chatChannelItems); } @@ -131,12 +142,24 @@ const InboxPage: FC = (props) => { } return items; - }, [chatChannelItems, sharedInboxItem]); + }, [chatChannelItems, sharedInboxItem, optimisticInboxFeedItems]); useUpdateEffect(() => { refetchInboxItems(); }, [isActiveUnreadInboxItemsQueryParam]); + useEffect(() => { + const firstFeedItem = topFeedItems[0]; + if(optimisticInboxFeedItems.size > 0 && firstFeedItem) { + + feedLayoutRef?.setActiveItem({ + feedItemId: firstFeedItem.itemId, + discussion: (firstFeedItem as FeedItemFollowLayoutItemWithFollowData)?.feedItem.optimisticData, + }); + } + + }, [topFeedItems, optimisticInboxFeedItems, feedLayoutRef]) + const fetchData = () => { fetchInboxData({ sharedFeedItemId, @@ -159,22 +182,26 @@ const InboxPage: FC = (props) => { [], ); + const instantDiscussionMessage = useSelector(selectInstantDiscussionMessagesOrder); + const handleFeedItemUpdate = useCallback( (item: CommonFeed, isRemoved: boolean) => { - dispatch( - inboxActions.updateFeedItem({ - item, - isRemoved, - }), - ); - - if (!isRemoved && item.data.lastMessage?.ownerId === userId) { - document - .getElementById("feedLayoutWrapper") - ?.scrollIntoView({ behavior: "smooth" }); + if(!instantDiscussionMessage.has(item.data.id)) { + dispatch( + inboxActions.updateFeedItem({ + item, + isRemoved, + }), + ); + + if (!isRemoved && item.data.lastMessage?.ownerId === userId) { + document + .getElementById("feedLayoutWrapper") + ?.scrollIntoView({ behavior: "smooth" }); + } } }, - [dispatch], + [dispatch, instantDiscussionMessage], ); const handleFeedItemUnfollowed = useCallback( diff --git a/src/shared/hooks/index.tsx b/src/shared/hooks/index.tsx index 7c78ecdd51..5371485a16 100644 --- a/src/shared/hooks/index.tsx +++ b/src/shared/hooks/index.tsx @@ -33,3 +33,5 @@ export { default as useImageSizeCheck } from "./useImageSizeCheck"; export { default as useLightThemeOnly } from "./useLightThemeOnly"; export * from "./useToggle"; export * from "./useTraceUpdate"; +export * from "./useElementPresence"; +export * from "./useIsElementFocused"; \ No newline at end of file diff --git a/src/shared/hooks/useCases/useCommonFeedItems.ts b/src/shared/hooks/useCases/useCommonFeedItems.ts index d527fe5f31..f4f27922eb 100644 --- a/src/shared/hooks/useCases/useCommonFeedItems.ts +++ b/src/shared/hooks/useCases/useCommonFeedItems.ts @@ -4,6 +4,7 @@ import { CommonFeedService } from "@/services"; import { commonActions, FeedItems, + optimisticActions, selectFeedItems, selectFilteredFeedItems, selectOptimisticFeedItems, @@ -65,7 +66,7 @@ export const useCommonFeedItems = ( const discussionId = item.commonFeedItem.data.discussionId ?? item.commonFeedItem.data.id; if(optItemIds.includes(discussionId)) { - dispatch(commonActions.removeOptimisticFeedItemState({id: discussionId})) + dispatch(optimisticActions.removeOptimisticFeedItemState({id: discussionId})) } }) diff --git a/src/shared/hooks/useCases/useInboxItems.ts b/src/shared/hooks/useCases/useInboxItems.ts index 19b5fde240..e9fefd0466 100644 --- a/src/shared/hooks/useCases/useInboxItems.ts +++ b/src/shared/hooks/useCases/useInboxItems.ts @@ -5,16 +5,23 @@ import { Logger, UserService } from "@/services"; import { addMetadataToItemsBatch } from "@/services/utils"; import { checkIsFeedItemFollowLayoutItemWithFollowData, + FeedItemFollowLayoutItemWithFollowData, FeedLayoutItemWithFollowData, + InboxItemBatch, InboxItemsBatch as ItemsBatch, } from "@/shared/interfaces"; import { InboxItem, Timestamp } from "@/shared/models"; import { inboxActions, InboxItems, + NewInboxItems, + optimisticActions, selectFilteredInboxItems, selectInboxItems, + selectInstantDiscussionMessagesOrder, + selectOptimisticInboxFeedItems, } from "@/store/states"; +import { useDeepCompareEffect } from "react-use"; interface Return extends Pick { @@ -67,6 +74,10 @@ export const useInboxItems = ( options?: { unread?: boolean }, ): Return => { const dispatch = useDispatch(); + const optimisticInboxItems = useSelector(selectOptimisticInboxFeedItems); + const instantDiscussionMessages = useSelector( + selectInstantDiscussionMessagesOrder, + ); const [newItemsBatches, setNewItemsBatches] = useState([]); const [lastUpdatedAt, setLastUpdatedAt] = useState(null); const inboxItems = useSelector(selectInboxItems); @@ -240,6 +251,33 @@ export const useInboxItems = ( unread, ]); + const [notListedFeedItems, setNotListedFeedItems] = useState[]>([]); + + useDeepCompareEffect(() => { + if(notListedFeedItems.length > 0 && notListedFeedItems.length === instantDiscussionMessages.size) { + const updatedFeedItems = notListedFeedItems.map((item) => { + const itemData = item?.item as FeedItemFollowLayoutItemWithFollowData; + const feedItemData = itemData.feedItem?.data; + const messageUpdatedAt = instantDiscussionMessages.get(feedItemData?.discussionId ?? "")?.timestamp || instantDiscussionMessages.get(feedItemData.id)?.timestamp; + return { + ...item, + item: { + ...itemData, + feedItem: { + ...itemData.feedItem, + updatedAt: messageUpdatedAt, + } + } + } + }) as NewInboxItems[]; + + setNotListedFeedItems([]); + dispatch(inboxActions.addNewInboxItems(updatedFeedItems)); + dispatch(optimisticActions.clearInstantDiscussionMessagesOrder()); + } + + },[notListedFeedItems, instantDiscussionMessages]); + useEffect(() => { if (!lastBatch || !userId) { return; @@ -260,7 +298,23 @@ export const useInboxItems = ( ); if (finalData.length > 0 && isMounted) { - dispatch(inboxActions.addNewInboxItems(finalData)); + const newItems: InboxItemBatch[] = []; + finalData.forEach((item: InboxItemBatch) => { + const itemData = (item.item as FeedItemFollowLayoutItemWithFollowData)?.feedItem?.data; + + if(instantDiscussionMessages.has(itemData?.discussionId ?? "") || instantDiscussionMessages.has(itemData?.id)) { + setNotListedFeedItems((prev) => [...prev, item]); + } else { + newItems.push(item); + } + if(optimisticInboxItems.has(itemData.id)) { + dispatch(optimisticActions.removeOptimisticInboxFeedItemState({id: itemData.id})); + } else if (itemData?.discussionId && optimisticInboxItems.has(itemData?.discussionId)) { + dispatch(optimisticActions.removeOptimisticInboxFeedItemState({id: itemData?.discussionId})); + } + }) + + newItems.length > 0 && dispatch(inboxActions.addNewInboxItems(newItems)); } } catch (error) { Logger.error(error); @@ -272,7 +326,8 @@ export const useInboxItems = ( return () => { isMounted = false; }; - }, [lastBatch]); + }, [lastBatch, instantDiscussionMessages]); + return { ...inboxItems, diff --git a/src/shared/hooks/useElementPresence.tsx b/src/shared/hooks/useElementPresence.tsx new file mode 100644 index 0000000000..27eb4f74ce --- /dev/null +++ b/src/shared/hooks/useElementPresence.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from "react"; + +export const useElementPresence = (attribute, value) => { + const [elementPresent, setElementPresent] = useState(false); + + useEffect(() => { + const observerCallback = (mutationsList) => { + mutationsList.forEach((mutation) => { + // Check for added nodes + mutation.addedNodes.forEach((node) => { + if ( + node.nodeType === 1 && // Ensure it's an element + node.getAttribute(attribute) === value + ) { + setElementPresent(true); + } + }); + + // Check for removed nodes + mutation.removedNodes.forEach((node) => { + if ( + node.nodeType === 1 && // Ensure it's an element + node.getAttribute(attribute) === value + ) { + setElementPresent(false); + } + }); + }); + }; + + const observer = new MutationObserver(observerCallback); + + // Start observing the entire document + observer.observe(document.body, { childList: true, subtree: true }); + + // Check initially if the element is already present + const initialElement = document.querySelector(`[${attribute}="${value}"]`); + if (initialElement) { + setElementPresent(true); + } + + return () => { + observer.disconnect(); // Cleanup observer on unmount + }; + }, [attribute, value]); + + return elementPresent; +}; \ No newline at end of file diff --git a/src/shared/hooks/useIsElementFocused.tsx b/src/shared/hooks/useIsElementFocused.tsx new file mode 100644 index 0000000000..da6dd02e97 --- /dev/null +++ b/src/shared/hooks/useIsElementFocused.tsx @@ -0,0 +1,24 @@ +import { useEffect, useState } from "react"; + +export const useIsElementFocused = (id: string) => { + const [isFocused, setIsFocused] = useState(false); + + useEffect(() => { + const handleFocusChange = () => { + const element = document.getElementById(id); + setIsFocused(document.activeElement === element); + }; + + // Listen to focus changes globally + document.addEventListener("focusin", handleFocusChange); + document.addEventListener("focusout", handleFocusChange); + + // Cleanup + return () => { + document.removeEventListener("focusin", handleFocusChange); + document.removeEventListener("focusout", handleFocusChange); + }; + }, [id]); + + return isFocused; +}; \ No newline at end of file diff --git a/src/shared/interfaces/State.tsx b/src/shared/interfaces/State.tsx index a1e49a4bb9..2adf19a158 100644 --- a/src/shared/interfaces/State.tsx +++ b/src/shared/interfaces/State.tsx @@ -8,6 +8,7 @@ import { ChatState, CommonFeedFollowsState, MultipleSpacesLayoutState, + OptimisticState } from "@/store/states"; import { AuthStateType } from "../../pages/Auth/interface"; import { CommonsStateType } from "../../pages/OldCommon/interfaces"; @@ -28,4 +29,5 @@ export interface AppState { chat: ChatState; inbox: InboxState; multipleSpacesLayout: MultipleSpacesLayoutState; + optimistic: OptimisticState; } diff --git a/src/shared/models/CommonFeed.tsx b/src/shared/models/CommonFeed.tsx index ef8cccabce..ccbe1095b0 100644 --- a/src/shared/models/CommonFeed.tsx +++ b/src/shared/models/CommonFeed.tsx @@ -28,6 +28,14 @@ export interface LastMessageContent { ownerType?: DiscussionMessageOwnerType; } +export interface LastMessageContentWithMessageId { + userName: string; + ownerId: string; + content: string; + ownerType?: DiscussionMessageOwnerType; + messageId: string; +} + export type DiscussionWithOptimisticData = Discussion & { state?: OptimisticFeedItemState; // Optional state property lastMessageContent: LastMessageContent; // Additional property diff --git a/src/shared/ui-kit/TextEditor/utils/countTextEditorEmojiElements.ts b/src/shared/ui-kit/TextEditor/utils/countTextEditorEmojiElements.ts index f8bdcd9ec8..828decab8a 100644 --- a/src/shared/ui-kit/TextEditor/utils/countTextEditorEmojiElements.ts +++ b/src/shared/ui-kit/TextEditor/utils/countTextEditorEmojiElements.ts @@ -14,6 +14,7 @@ export const countTextEditorEmojiElements = ( let hasText = false; let emojiCount = 0; + editorValue.forEach((element) => { if ( (element as ParagraphElement)?.type === ElementType.Paragraph && diff --git a/src/shared/utils/generateOptimisticFeedItem.ts b/src/shared/utils/generateOptimisticFeedItem.ts index df9731e3d9..18ce7a2e51 100644 --- a/src/shared/utils/generateOptimisticFeedItem.ts +++ b/src/shared/utils/generateOptimisticFeedItem.ts @@ -1,6 +1,6 @@ import { Timestamp as FirestoreTimestamp } from "firebase/firestore"; import { v4 as uuidv4 } from "uuid"; -import { CommonFeed, CommonFeedType, LastMessageContent, OptimisticFeedItemState } from "../models"; +import { Common, CommonFeed, CommonFeedType, FeedItemFollowWithMetadata, LastMessageContent, OptimisticFeedItemState } from "../models"; interface GenerateOptimisticFeedItemPayload { userId: string; @@ -63,4 +63,29 @@ export const generateOptimisticFeedItem = ({ }, circleVisibility, } +} + +export const generateOptimisticFeedItemFollowWithMetadata = ({ feedItem, common }: { + feedItem: CommonFeed, + common: Common; +}): FeedItemFollowWithMetadata => { + + const currentDate = FirestoreTimestamp.now(); + return { + commonAvatar: common.image, + commonId: common.id, + commonName: common.name, + feedItem: feedItem, + feedItemId: feedItem.id, + userId: feedItem.userId, + emailSubscribed: false, + pushSubscribed: false, + count: 0, + type: feedItem.data.type, + lastSeen: currentDate, + createdAt: currentDate, + updatedAt: currentDate, + lastActivity: currentDate, + id: feedItem.id, + } } \ No newline at end of file diff --git a/src/store/reducer.tsx b/src/store/reducer.tsx index 7d885610bf..cf96f039e9 100644 --- a/src/store/reducer.tsx +++ b/src/store/reducer.tsx @@ -15,6 +15,7 @@ import { multipleSpacesLayoutReducer, projectsReducer, chatReducer, + optimisticReducer } from "./states"; export default (history: History) => { @@ -32,6 +33,7 @@ export default (history: History) => { chat: chatReducer, inbox: inboxReducer, multipleSpacesLayout: multipleSpacesLayoutReducer, + optimistic: optimisticReducer, }); return (state: AppState | undefined, action: AnyAction) => { diff --git a/src/store/states/common/actions.ts b/src/store/states/common/actions.ts index df2c86b1d7..ef1848fec1 100644 --- a/src/store/states/common/actions.ts +++ b/src/store/states/common/actions.ts @@ -17,7 +17,7 @@ import { CommonMember, Discussion, Governance, - OptimisticFeedItemState, + LastMessageContentWithMessageId, Proposal, } from "@/shared/models"; import { CommonActionType } from "./constants"; @@ -28,7 +28,6 @@ import { FeedItemsPayload, PinnedFeedItems, } from "./types"; -import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; export const resetCommon = createStandardAction( CommonActionType.RESET_COMMON, @@ -220,35 +219,6 @@ export const setSharedFeedItem = createStandardAction( CommonActionType.SET_SHARED_FEED_ITEM, )(); -export const setOptimisticFeedItem = createStandardAction( - CommonActionType.SET_OPTIMISTIC_FEED_ITEM, -)(); - -export const updateOptimisticFeedItemState = createStandardAction( - CommonActionType.UPDATE_OPTIMISTIC_FEED_ITEM, -)<{ - id: string; - state: OptimisticFeedItemState; -}>(); - -export const removeOptimisticFeedItemState = createStandardAction( - CommonActionType.REMOVE_OPTIMISTIC_FEED_ITEM, -)<{ - id: string; -}>(); - -export const setOptimisticDiscussionMessages = createStandardAction( - CommonActionType.SET_OPTIMISTIC_DISCUSSION_MESSAGES, -)(); - -export const clearOptimisticDiscussionMessages = createStandardAction( - CommonActionType.CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES, -)(); - -export const clearCreatedOptimisticFeedItem = createStandardAction( - CommonActionType.CLEAR_CREATED_OPTIMISTIC_FEED_ITEM, -)(); - export const setRecentStreamId = createStandardAction( CommonActionType.SET_RECENT_STREAM_ID, )(); @@ -263,3 +233,10 @@ export const setRecentAssignedCircleByMember = createStandardAction( export const resetRecentAssignedCircleByMember = createStandardAction( CommonActionType.RESET_RECENT_ASSIGNED_CIRCLE_BY_MEMBER, )(); + +export const setFeedItemUpdatedAt = createStandardAction( + CommonActionType.SET_FEED_ITEM_UPDATED_AT, +)<{ + feedItemId: string; + lastMessage: LastMessageContentWithMessageId; +}>(); diff --git a/src/store/states/common/constants.ts b/src/store/states/common/constants.ts index 4423ef7835..94a9e725f6 100644 --- a/src/store/states/common/constants.ts +++ b/src/store/states/common/constants.ts @@ -56,17 +56,10 @@ export enum CommonActionType { SET_SHARED_FEED_ITEM_ID = "@COMMON/SET_SHARED_FEED_ITEM_ID", SET_SHARED_FEED_ITEM = "@COMMON/SET_SHARED_FEED_ITEM", - SET_OPTIMISTIC_FEED_ITEM = "@COMMON/SET_OPTIMISTIC_FEED_ITEM", - UPDATE_OPTIMISTIC_FEED_ITEM = "@COMMON/UPDATE_OPTIMISTIC_FEED_ITEM", - REMOVE_OPTIMISTIC_FEED_ITEM = "@COMMON/REMOVE_OPTIMISTIC_FEED_ITEM", - - SET_OPTIMISTIC_DISCUSSION_MESSAGES = "@COMMON/SET_OPTIMISTIC_DISCUSSION_MESSAGES", - CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES = "@COMMON/CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES", - - CLEAR_CREATED_OPTIMISTIC_FEED_ITEM = "@COMMON/CLEAR_CREATED_OPTIMISTIC_FEED_ITEM", - SET_RECENT_STREAM_ID = "@COMMON/SET_RECENT_STREAM_ID", SET_RECENT_ASSIGNED_CIRCLE_BY_MEMBER = "@COMMON/SET_RECENT_ASSIGNED_CIRCLE_BY_MEMBER", RESET_RECENT_ASSIGNED_CIRCLE_BY_MEMBER = "@COMMON/RESET_RECENT_ASSIGNED_CIRCLE_BY_MEMBER", + + SET_FEED_ITEM_UPDATED_AT = "@COMMON/SET_FEED_ITEM_UPDATED_AT", } diff --git a/src/store/states/common/reducer.ts b/src/store/states/common/reducer.ts index a5b23ff3fd..578d6d0f66 100644 --- a/src/store/states/common/reducer.ts +++ b/src/store/states/common/reducer.ts @@ -7,7 +7,7 @@ import { deserializeFeedItemFollowLayoutItem, FeedItemFollowLayoutItem, } from "@/shared/interfaces"; -import { CommonFeed } from "@/shared/models"; +import { CommonFeed, Timestamp } from "@/shared/models"; import { areTimestampsEqual, convertToTimestamp, @@ -50,9 +50,6 @@ const initialState: CommonState = { searchState: { ...initialSearchState }, sharedFeedItemId: null, sharedFeedItem: null, - optimisticFeedItems: new Map(), - optimisticDiscussionMessages: new Map(), - createdOptimisticFeedItems: new Map(), commonAction: null, discussionCreation: { data: null, @@ -696,90 +693,31 @@ export const reducer = createReducer(initialState) : null; }), ) - .handleAction(actions.setOptimisticFeedItem, (state, { payload }) => + .handleAction(actions.setFeedItemUpdatedAt, (state, { payload }) => produce(state, (nextState) => { - const updatedMap = new Map(nextState.optimisticFeedItems); + const feedItemId = payload.feedItemId; - const optimisticItemId = payload.data.discussionId ?? payload.data.id; - // Add the new item to the Map - updatedMap.set(optimisticItemId, { - type: InboxItemType.FeedItemFollow, - itemId: payload.id, - feedItem: payload, - }); - - // Assign the new Map back to the state - nextState.optimisticFeedItems = updatedMap; - nextState.recentStreamId = optimisticItemId; - }), - ) - .handleAction(actions.updateOptimisticFeedItemState, (state, { payload }) => - produce(state, (nextState) => { - const updatedMap = new Map(nextState.optimisticFeedItems); - - const optimisticFeedItem = updatedMap.get(payload.id); - // Add the new item to the Map + const updatedFeedItemIndex = nextState.feedItems.data?.findIndex( + feedItem => feedItem.itemId === feedItemId + ) ?? -1; - if(optimisticFeedItem && optimisticFeedItem?.feedItem.optimisticData) { - updatedMap.set(payload.id, { - ...optimisticFeedItem, - feedItem: { - ...optimisticFeedItem?.feedItem, - optimisticData: { - ...optimisticFeedItem.feedItem.optimisticData, - state: payload.state - } + if (updatedFeedItemIndex !== -1 && nextState.feedItems.data) { + const item = nextState.feedItems.data[updatedFeedItemIndex]; + item.feedItem = { + ...item.feedItem, + updatedAt: Timestamp.fromDate(new Date()), + data: { + ...item.feedItem.data, + lastMessage: payload.lastMessage, } + }; + + // Sort `nextState.items.data` by `updatedAt` in descending order + nextState.feedItems.data.sort((a, b) => { + const dateA = a.feedItem.updatedAt.toDate().getTime(); + const dateB = b.feedItem.updatedAt.toDate().getTime(); + return dateB - dateA; // Sort in descending order }); } - - // Assign the new Map back to the state - nextState.optimisticFeedItems = updatedMap; - }), - ) - .handleAction(actions.removeOptimisticFeedItemState, (state, { payload }) => - produce(state, (nextState) => { - const createdOptimisticFeedItemsMap = new Map(nextState.createdOptimisticFeedItems); - const updatedMap = new Map(nextState.optimisticFeedItems); - - createdOptimisticFeedItemsMap.set(payload.id, updatedMap.get(payload.id)); - updatedMap.delete(payload.id); - - // Assign the new Map back to the state - nextState.optimisticFeedItems = updatedMap; - nextState.createdOptimisticFeedItems = createdOptimisticFeedItemsMap; }), - ) - .handleAction(actions.setOptimisticDiscussionMessages, (state, { payload }) => - produce(state, (nextState) => { - const updatedMap = new Map(nextState.optimisticDiscussionMessages); - - const discussionMessages = updatedMap.get(payload.discussionId) ?? []; - discussionMessages.push(payload); - // Add the new item to the Map - updatedMap.set(payload.discussionId, discussionMessages); - - // Assign the new Map back to the state - nextState.optimisticDiscussionMessages = updatedMap; - }), - ) - .handleAction(actions.clearOptimisticDiscussionMessages, (state, { payload }) => - produce(state, (nextState) => { - const updatedMap = new Map(nextState.optimisticDiscussionMessages); - - updatedMap.delete(payload); - - // Assign the new Map back to the state - nextState.optimisticDiscussionMessages = updatedMap; - }), - ) - .handleAction(actions.clearCreatedOptimisticFeedItem, (state, { payload }) => - produce(state, (nextState) => { - const updatedMap = new Map(nextState.createdOptimisticFeedItems); - - updatedMap.delete(payload); - - // Assign the new Map back to the state - nextState.createdOptimisticFeedItems = updatedMap; - }), - ); + ); \ No newline at end of file diff --git a/src/store/states/common/selectors.ts b/src/store/states/common/selectors.ts index 541fd87c23..025cf24e45 100644 --- a/src/store/states/common/selectors.ts +++ b/src/store/states/common/selectors.ts @@ -48,15 +48,6 @@ export const selectSharedFeedItem = (state: AppState) => export const selectRecentStreamId = (state: AppState) => state.common.recentStreamId; -export const selectOptimisticFeedItems = (state: AppState) => - state.common.optimisticFeedItems; - -export const selectOptimisticDiscussionMessages = (state: AppState) => - state.common.optimisticDiscussionMessages; - -export const selectCreatedOptimisticFeedItems = (state: AppState) => - state.common.createdOptimisticFeedItems; - export const selectRecentAssignedCircle = (memberId: string) => (state: AppState) => state.common.recentAssignedCircleByMember[memberId]; diff --git a/src/store/states/common/types.ts b/src/store/states/common/types.ts index d21b4d50b0..0d9eee745d 100644 --- a/src/store/states/common/types.ts +++ b/src/store/states/common/types.ts @@ -4,7 +4,6 @@ import { NewDiscussionCreationFormValues, NewProposalCreationFormValues, } from "@/shared/interfaces"; -import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; import { Circle, CommonMember, Governance, Timestamp } from "@/shared/models"; export type EntityCreation = { @@ -45,9 +44,6 @@ export interface CommonState { pinnedFeedItems: PinnedFeedItems; sharedFeedItemId: string | null; sharedFeedItem: FeedItemFollowLayoutItem | null; - createdOptimisticFeedItems: Map; - optimisticFeedItems: Map; - optimisticDiscussionMessages: Map; commonAction: CommonAction | null; discussionCreation: EntityCreation; proposalCreation: EntityCreation; diff --git a/src/store/states/inbox/actions.ts b/src/store/states/inbox/actions.ts index 77f59adaec..004ad09bee 100644 --- a/src/store/states/inbox/actions.ts +++ b/src/store/states/inbox/actions.ts @@ -1,8 +1,8 @@ import { createAsyncAction, createStandardAction } from "typesafe-actions"; import { FeedLayoutItemWithFollowData } from "@/shared/interfaces"; -import { ChatChannel, CommonFeed } from "@/shared/models"; +import { ChatChannel, CommonFeed, LastMessageContentWithMessageId } from "@/shared/models"; import { InboxActionType } from "./constants"; -import { InboxItems, InboxSearchState } from "./types"; +import { InboxItems, InboxSearchState, NewInboxItems } from "./types"; export const resetInbox = createStandardAction(InboxActionType.RESET_INBOX)<{ onlyIfUnread?: boolean; @@ -26,15 +26,7 @@ export const getInboxItems = createAsyncAction( export const addNewInboxItems = createStandardAction( InboxActionType.ADD_NEW_INBOX_ITEMS, -)< - { - item: FeedLayoutItemWithFollowData; - statuses: { - isAdded: boolean; - isRemoved: boolean; - }; - }[] ->(); +)(); export const updateInboxItem = createStandardAction( InboxActionType.UPDATE_INBOX_ITEM, @@ -119,3 +111,10 @@ export const removeEmptyChatChannelItems = createStandardAction( export const saveLastState = createStandardAction( InboxActionType.SAVE_LAST_STATE, )<{ shouldSaveAsReadState: boolean }>(); + +export const setInboxItemUpdatedAt = createStandardAction( + InboxActionType.SET_INBOX_ITEM_UPDATED_AT, +)<{ + feedItemId: string; + lastMessage: LastMessageContentWithMessageId; +}>(); diff --git a/src/store/states/inbox/constants.ts b/src/store/states/inbox/constants.ts index 6d5e907ee8..d51c475757 100644 --- a/src/store/states/inbox/constants.ts +++ b/src/store/states/inbox/constants.ts @@ -33,4 +33,6 @@ export enum InboxActionType { REMOVE_EMPTY_CHAT_CHANNEL_ITEMS = "@INBOX/REMOVE_EMPTY_CHAT_CHANNEL_ITEMS", SAVE_LAST_STATE = "@INBOX/SAVE_LAST_STATE", + + SET_INBOX_ITEM_UPDATED_AT = "@INBOX/SET_INBOX_ITEM_UPDATED_AT", } diff --git a/src/store/states/inbox/reducer.ts b/src/store/states/inbox/reducer.ts index a0b3fac21f..711cbaca20 100644 --- a/src/store/states/inbox/reducer.ts +++ b/src/store/states/inbox/reducer.ts @@ -6,6 +6,7 @@ import { InboxItemType, QueryParamKey } from "@/shared/constants"; import { checkIsChatChannelLayoutItem, checkIsFeedItemFollowLayoutItem, + FeedItemFollowLayoutItemWithFollowData, FeedLayoutItemWithFollowData, } from "@/shared/interfaces"; import { ChatChannel, CommonFeed, Timestamp } from "@/shared/models"; @@ -441,6 +442,13 @@ const updateChatChannelItem = ( updateChatChannelItemInSharedInboxItem(state, payload); }; +// Add type guard to check if an item is of type `FeedItemFollowLayoutItemWithFollowData` +function isFeedItemFollowLayoutItemWithFollowData( + item: FeedLayoutItemWithFollowData +): item is FeedItemFollowLayoutItemWithFollowData { + return (item as FeedItemFollowLayoutItemWithFollowData).feedItemFollowWithMetadata !== undefined; +} + export const reducer = createReducer(INITIAL_INBOX_STATE) .handleAction(actions.resetInbox, (state, { payload }) => { if (payload?.onlyIfUnread && !state.items.unread) { @@ -775,4 +783,39 @@ export const reducer = createReducer(INITIAL_INBOX_STATE) nextState.lastUnreadState = stateToSave; } }), + ) + .handleAction(actions.setInboxItemUpdatedAt, (state, { payload }) => + produce(state, (nextState) => { + const feedItemId = payload.feedItemId; + + const updatedFeedItemIndex = nextState.items.data?.findIndex( + feedItem => feedItem.itemId === feedItemId + ) ?? -1; + + if (updatedFeedItemIndex !== -1 && nextState.items.data) { + const item = nextState.items.data[updatedFeedItemIndex]; + + if (isFeedItemFollowLayoutItemWithFollowData(item)) { + item.feedItem = { + ...item.feedItem, + updatedAt: Timestamp.fromDate(new Date()), + data: { + ...item.feedItem.data, + lastMessage: payload.lastMessage, + } + }; + + // Sort `nextState.items.data` by `updatedAt` in descending order + nextState.items.data.sort((a, b) => { + const dateA = isFeedItemFollowLayoutItemWithFollowData(a) + ? a.feedItem.updatedAt.toDate().getTime() + : 0; // Use 0 for items without updatedAt + const dateB = isFeedItemFollowLayoutItemWithFollowData(b) + ? b.feedItem.updatedAt.toDate().getTime() + : 0; // Use 0 for items without updatedAt + return dateB - dateA; // Sort in descending order + }); + } + } + }) ); diff --git a/src/store/states/inbox/types.ts b/src/store/states/inbox/types.ts index 58542ac2a3..c18c75a673 100644 --- a/src/store/states/inbox/types.ts +++ b/src/store/states/inbox/types.ts @@ -20,6 +20,14 @@ export interface InboxItems { unread: boolean; } +export interface NewInboxItems { + item: FeedLayoutItemWithFollowData; + statuses: { + isAdded: boolean; + isRemoved: boolean; + }; +} + export type LastState = Pick< InboxState, | "items" @@ -39,3 +47,4 @@ export interface InboxState { lastReadState: LastState | null; lastUnreadState: LastState | null; } + diff --git a/src/store/states/index.ts b/src/store/states/index.ts index 0cb8d2e2b6..3ad06a46bb 100644 --- a/src/store/states/index.ts +++ b/src/store/states/index.ts @@ -6,3 +6,4 @@ export * from "./inbox"; export * from "./multipleSpacesLayout"; export * from "./projects"; export * from "./chat"; +export * from "./optimistic"; diff --git a/src/store/states/optimistic/actions.ts b/src/store/states/optimistic/actions.ts new file mode 100644 index 0000000000..4a94826bb9 --- /dev/null +++ b/src/store/states/optimistic/actions.ts @@ -0,0 +1,60 @@ +import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; +import { + Common, + CommonFeed, + OptimisticFeedItemState +} from "@/shared/models"; +import { createStandardAction } from "typesafe-actions"; +import { OptimisticActionType } from "./constants"; + +export const setOptimisticFeedItem = createStandardAction( + OptimisticActionType.SET_OPTIMISTIC_FEED_ITEM, +)<{ + data: CommonFeed; + common: Common; +}>(); + +export const updateOptimisticFeedItemState = createStandardAction( + OptimisticActionType.UPDATE_OPTIMISTIC_FEED_ITEM, +)<{ + id: string; + state: OptimisticFeedItemState; +}>(); + +export const removeOptimisticFeedItemState = createStandardAction( + OptimisticActionType.REMOVE_OPTIMISTIC_FEED_ITEM, +)<{ + id: string; +}>(); + +export const removeOptimisticInboxFeedItemState = createStandardAction( + OptimisticActionType.REMOVE_OPTIMISTIC_INBOX_FEED_ITEM, +)<{ + id: string; +}>(); + +export const setOptimisticDiscussionMessages = createStandardAction( + OptimisticActionType.SET_OPTIMISTIC_DISCUSSION_MESSAGES, +)(); + +export const clearOptimisticDiscussionMessages = createStandardAction( + OptimisticActionType.CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES, +)(); + +export const setInstantDiscussionMessagesOrder = createStandardAction( + OptimisticActionType.SET_INSTANT_DISCUSSION_MESSAGES_ORDER, +)<{ + discussionId: string; +}>(); + +export const clearInstantDiscussionMessagesOrder = createStandardAction( + OptimisticActionType.CLEAR_INSTANT_DISCUSSION_MESSAGES_ORDER, +)(); + +export const clearCreatedOptimisticFeedItem = createStandardAction( + OptimisticActionType.CLEAR_CREATED_OPTIMISTIC_FEED_ITEM, +)(); + +export const resetOptimisticState = createStandardAction( + OptimisticActionType.RESET_OPTIMISTIC_STATE, +)(); \ No newline at end of file diff --git a/src/store/states/optimistic/constants.ts b/src/store/states/optimistic/constants.ts new file mode 100644 index 0000000000..9bca26b879 --- /dev/null +++ b/src/store/states/optimistic/constants.ts @@ -0,0 +1,15 @@ +export enum OptimisticActionType { + SET_OPTIMISTIC_FEED_ITEM = "@OPTIMISTIC/SET_OPTIMISTIC_FEED_ITEM", + UPDATE_OPTIMISTIC_FEED_ITEM = "@OPTIMISTIC/UPDATE_OPTIMISTIC_FEED_ITEM", + REMOVE_OPTIMISTIC_FEED_ITEM = "@OPTIMISTIC/REMOVE_OPTIMISTIC_FEED_ITEM", + REMOVE_OPTIMISTIC_INBOX_FEED_ITEM = "@OPTIMISTIC/REMOVE_OPTIMISTIC_INBOX_FEED_ITEM", + + SET_OPTIMISTIC_DISCUSSION_MESSAGES = "@OPTIMISTIC/SET_OPTIMISTIC_DISCUSSION_MESSAGES", + CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES = "@OPTIMISTIC/CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES", + + SET_INSTANT_DISCUSSION_MESSAGES_ORDER = "@OPTIMISTIC/SET_INSTANT_DISCUSSION_MESSAGES_ORDER", + CLEAR_INSTANT_DISCUSSION_MESSAGES_ORDER = "@OPTIMISTIC/CLEAR_INSTANT_DISCUSSION_MESSAGES_ORDER", + + CLEAR_CREATED_OPTIMISTIC_FEED_ITEM = "@OPTIMISTIC/CLEAR_CREATED_OPTIMISTIC_FEED_ITEM", + RESET_OPTIMISTIC_STATE = "RESET_OPTIMISTIC_STATE", +} diff --git a/src/store/states/optimistic/index.ts b/src/store/states/optimistic/index.ts new file mode 100644 index 0000000000..85d83b51d7 --- /dev/null +++ b/src/store/states/optimistic/index.ts @@ -0,0 +1,4 @@ +export * as optimisticActions from "./actions"; +export { reducer as optimisticReducer } from "./reducer"; +export * from "./selectors"; +export * from "./types"; diff --git a/src/store/states/optimistic/reducer.ts b/src/store/states/optimistic/reducer.ts new file mode 100644 index 0000000000..a437af2cde --- /dev/null +++ b/src/store/states/optimistic/reducer.ts @@ -0,0 +1,179 @@ +import { InboxItemType } from "@/shared/constants"; +import produce from "immer"; +import { ActionType, createReducer } from "typesafe-actions"; +import * as actions from "./actions"; +import { + OptimisticState +} from "./types"; +import { generateOptimisticFeedItemFollowWithMetadata } from "@/shared/utils"; +import { Timestamp } from "@/shared/models"; + +type Action = ActionType; + +const initialState: OptimisticState = { + optimisticFeedItems: new Map(), + optimisticInboxFeedItems: new Map(), + optimisticDiscussionMessages: new Map(), + createdOptimisticFeedItems: new Map(), + instantDiscussionMessagesOrder: new Map(), +}; + +export const reducer = createReducer(initialState) + .handleAction(actions.resetOptimisticState, () => initialState) + .handleAction(actions.setInstantDiscussionMessagesOrder, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.instantDiscussionMessagesOrder); + const { discussionId } = payload; + + if(updatedMap.size > 1 && updatedMap.has(discussionId)) { + const keys = Array.from(updatedMap.keys()); + + keys.forEach((key) => { + const orderValue = updatedMap.get(key)?.order || 2; + const timestampValue = updatedMap.get(key)?.timestamp || Timestamp.fromDate(new Date()); + updatedMap.set(key, { + order: orderValue === 1 ? 1 : orderValue - 1, + timestamp: timestampValue + }); + }); + } + + if(updatedMap.has(discussionId)) { + updatedMap.set(discussionId, { + order: updatedMap.size || 1, + timestamp: Timestamp.fromDate(new Date()) + }); + } else { + updatedMap.set(discussionId, { + order: (updatedMap.size + 1) || 1, + timestamp: Timestamp.fromDate(new Date()) + }); + } + nextState.instantDiscussionMessagesOrder = updatedMap; + }), + ) + .handleAction(actions.clearInstantDiscussionMessagesOrder, (state) => + produce(state, (nextState) => { + const updatedMap = new Map(); + + // updatedMap.delete(payload); + + // Assign the new Map back to the state + nextState.instantDiscussionMessagesOrder = updatedMap; + }), + ) + .handleAction(actions.setOptimisticFeedItem, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.optimisticFeedItems); + const updateMapInbox = new Map(nextState.optimisticInboxFeedItems); + + const optimisticItemId = payload.data.data.discussionId ?? payload.data.data.id; + // Add the new item to the Map + updatedMap.set(optimisticItemId, { + type: InboxItemType.FeedItemFollow, + itemId: payload.data.id, + feedItem: payload.data, + }); + updateMapInbox.set(optimisticItemId, { + type: InboxItemType.FeedItemFollow, + itemId: payload.data.id, + feedItem: payload.data, + feedItemFollowWithMetadata: generateOptimisticFeedItemFollowWithMetadata({feedItem: payload.data, common: payload.common}) + }); + // Assign the new Map back to the state + nextState.optimisticFeedItems = updatedMap; + nextState.optimisticInboxFeedItems = updateMapInbox; + }), + ) + .handleAction(actions.updateOptimisticFeedItemState, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.optimisticFeedItems); + const updateMapInbox = new Map(nextState.optimisticInboxFeedItems); + + const optimisticFeedItem = updatedMap.get(payload.id); + // Add the new item to the Map + + if(optimisticFeedItem && optimisticFeedItem?.feedItem.optimisticData) { + updatedMap.set(payload.id, { + ...optimisticFeedItem, + feedItem: { + ...optimisticFeedItem?.feedItem, + optimisticData: { + ...optimisticFeedItem.feedItem.optimisticData, + state: payload.state + } + } + }); + + updateMapInbox.set(payload.id, { + ...optimisticFeedItem, + feedItem: { + ...optimisticFeedItem?.feedItem, + optimisticData: { + ...optimisticFeedItem.feedItem.optimisticData, + state: payload.state + } + } + }); + } + + // Assign the new Map back to the state + nextState.optimisticFeedItems = updatedMap; + nextState.optimisticInboxFeedItems = updateMapInbox; + }), + ) + .handleAction(actions.removeOptimisticFeedItemState, (state, { payload }) => + produce(state, (nextState) => { + const createdOptimisticFeedItemsMap = new Map(nextState.createdOptimisticFeedItems); + const updatedMap = new Map(nextState.optimisticFeedItems); + + createdOptimisticFeedItemsMap.set(payload.id, updatedMap.get(payload.id)); + updatedMap.delete(payload.id); + + // Assign the new Map back to the state + nextState.optimisticFeedItems = updatedMap; + nextState.createdOptimisticFeedItems = createdOptimisticFeedItemsMap; + }), + ) + .handleAction(actions.removeOptimisticInboxFeedItemState, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.optimisticInboxFeedItems); + updatedMap.delete(payload.id); + + // Assign the new Map back to the state + nextState.optimisticInboxFeedItems = updatedMap; + }), + ) + .handleAction(actions.setOptimisticDiscussionMessages, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.optimisticDiscussionMessages); + + const discussionMessages = updatedMap.get(payload.discussionId) ?? []; + discussionMessages.push(payload); + // Add the new item to the Map + updatedMap.set(payload.discussionId, discussionMessages); + + // Assign the new Map back to the state + nextState.optimisticDiscussionMessages = updatedMap; + }), + ) + .handleAction(actions.clearOptimisticDiscussionMessages, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.optimisticDiscussionMessages); + + updatedMap.delete(payload); + + // Assign the new Map back to the state + nextState.optimisticDiscussionMessages = updatedMap; + }), + ) + .handleAction(actions.clearCreatedOptimisticFeedItem, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.createdOptimisticFeedItems); + + updatedMap.delete(payload); + + // Assign the new Map back to the state + nextState.createdOptimisticFeedItems = updatedMap; + }), + ); \ No newline at end of file diff --git a/src/store/states/optimistic/selectors.ts b/src/store/states/optimistic/selectors.ts new file mode 100644 index 0000000000..bed310b5ea --- /dev/null +++ b/src/store/states/optimistic/selectors.ts @@ -0,0 +1,17 @@ +import { AppState } from "@/shared/interfaces"; + + +export const selectOptimisticFeedItems = (state: AppState) => + state.optimistic.optimisticFeedItems; + +export const selectOptimisticInboxFeedItems = (state: AppState) => + state.optimistic.optimisticInboxFeedItems; + +export const selectOptimisticDiscussionMessages = (state: AppState) => + state.optimistic.optimisticDiscussionMessages; + +export const selectCreatedOptimisticFeedItems = (state: AppState) => + state.optimistic.createdOptimisticFeedItems; + +export const selectInstantDiscussionMessagesOrder = (state: AppState) => + state.optimistic.instantDiscussionMessagesOrder; diff --git a/src/store/states/optimistic/types.ts b/src/store/states/optimistic/types.ts new file mode 100644 index 0000000000..fe11016df9 --- /dev/null +++ b/src/store/states/optimistic/types.ts @@ -0,0 +1,16 @@ +import { + FeedItemFollowLayoutItem +} from "@/shared/interfaces"; +import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; +import { Timestamp } from "@/shared/models"; + +export interface OptimisticState { + createdOptimisticFeedItems: Map; + optimisticFeedItems: Map; + optimisticInboxFeedItems: Map; + optimisticDiscussionMessages: Map; + instantDiscussionMessagesOrder: Map; +} diff --git a/src/store/store.tsx b/src/store/store.tsx index b56c38a915..9154839722 100644 --- a/src/store/store.tsx +++ b/src/store/store.tsx @@ -23,11 +23,66 @@ import { cacheTransform, multipleSpacesLayoutTransform, } from "./transforms"; +import { createTransform } from "redux-persist"; +import { OptimisticState } from "./states"; +import { FeedItemFollowLayoutItem } from "@/shared/interfaces"; +import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; + +// Define the keys in CommonState that are of type Map +const mapFields: Array = [ + "createdOptimisticFeedItems", + "optimisticFeedItems", + "optimisticInboxFeedItems", + "optimisticDiscussionMessages", + "instantDiscussionMessagesOrder" +]; + +const mapTransformer = createTransform( + (inboundState) => { + const transformedState: OptimisticState = { ...inboundState }; + + mapFields.forEach((key) => { + const value = inboundState[key]; + if (value instanceof Map) { + if (key === "optimisticDiscussionMessages") { + (transformedState as any)[key] = Array.from( + (value as Map).entries() + ) as [string, CreateDiscussionMessageDto[]][]; + } else { + (transformedState as any)[key] = Array.from( + (value as Map).entries() + ) as [string, FeedItemFollowLayoutItem | undefined][]; + } + } + }); + + return transformedState; + }, + (outboundState) => { + const transformedState: OptimisticState = { ...outboundState }; + + mapFields.forEach((key) => { + const value = outboundState[key]; + if (Array.isArray(value)) { + if (key === "optimisticDiscussionMessages") { + (transformedState as any)[key] = new Map(value) as Map; + } else { + (transformedState as any)[key] = new Map(value) as Map; + } + } + }); + + return transformedState; + }, + { whitelist: ["optimistic"] } +); + const persistConfig: PersistConfig = { key: "root", storage, whitelist: [ + "optimistic", "projects", "commonLayout", "commonFeedFollows", @@ -36,7 +91,7 @@ const persistConfig: PersistConfig = { "multipleSpacesLayout", ], stateReconciler: autoMergeLevel2, - transforms: [inboxTransform, cacheTransform, multipleSpacesLayoutTransform], + transforms: [mapTransformer, inboxTransform, cacheTransform, multipleSpacesLayoutTransform], }; const sagaMiddleware = createSagaMiddleware();