From ce05830205cbd560c180e494a9468f057e51afae Mon Sep 17 00:00:00 2001 From: Pavel Meyer <meyerpavel1207@gmail.com> Date: Mon, 4 Nov 2024 15:13:39 +0300 Subject: [PATCH 1/6] CW-instant-reorder Added instant reorder for feedItems and inbox items --- .../ChatComponent/ChatComponent.tsx | 16 +++++++ src/pages/inbox/BaseInbox.tsx | 1 + src/shared/models/CommonFeed.tsx | 8 ++++ src/store/states/common/actions.ts | 8 ++++ src/store/states/common/constants.ts | 2 + src/store/states/common/reducer.ts | 30 ++++++++++++- src/store/states/inbox/actions.ts | 9 +++- src/store/states/inbox/constants.ts | 2 + src/store/states/inbox/reducer.ts | 43 +++++++++++++++++++ 9 files changed, 117 insertions(+), 2 deletions(-) diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 5bd1d392d..b833ff979 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -65,6 +65,7 @@ import { selectOptimisticFeedItems, commonActions, selectOptimisticDiscussionMessages, + inboxActions, } from "@/store/states"; import { ChatContentContext, ChatContentData } from "../CommonContent/context"; import { @@ -606,6 +607,20 @@ 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)); focusOnChat(); } }, @@ -620,6 +635,7 @@ export default function ChatComponent({ isChatChannel, linkPreviewData, isOptimisticChat, + feedItemId, ], ); diff --git a/src/pages/inbox/BaseInbox.tsx b/src/pages/inbox/BaseInbox.tsx index 6b34a11d8..c0764254e 100644 --- a/src/pages/inbox/BaseInbox.tsx +++ b/src/pages/inbox/BaseInbox.tsx @@ -102,6 +102,7 @@ const InboxPage: FC<InboxPageProps> = (props) => { } = useInboxItems(feedItemIdsForNotListening, { unread: isActiveUnreadInboxItemsQueryParam, }); + const sharedInboxItem = useSelector(selectSharedInboxItem); const chatChannelItems = useSelector(selectChatChannelItems); const nextChatChannelItemId = useSelector(selectNextChatChannelItemId); diff --git a/src/shared/models/CommonFeed.tsx b/src/shared/models/CommonFeed.tsx index ef8cccabc..ccbe1095b 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/store/states/common/actions.ts b/src/store/states/common/actions.ts index df2c86b1d..cedc09af7 100644 --- a/src/store/states/common/actions.ts +++ b/src/store/states/common/actions.ts @@ -17,6 +17,7 @@ import { CommonMember, Discussion, Governance, + LastMessageContentWithMessageId, OptimisticFeedItemState, Proposal, } from "@/shared/models"; @@ -263,3 +264,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 4423ef783..fb0c82dd4 100644 --- a/src/store/states/common/constants.ts +++ b/src/store/states/common/constants.ts @@ -69,4 +69,6 @@ export enum CommonActionType { 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 a5b23ff3f..6bca6584c 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, @@ -782,4 +782,32 @@ export const reducer = createReducer<CommonState, Action>(initialState) // Assign the new Map back to the state nextState.createdOptimisticFeedItems = updatedMap; }), + ) + .handleAction(actions.setFeedItemUpdatedAt, (state, { payload }) => + produce(state, (nextState) => { + const feedItemId = payload.feedItemId; + + const updatedFeedItemIndex = nextState.feedItems.data?.findIndex( + feedItem => feedItem.itemId === feedItemId + ) ?? -1; + + 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 + }); + } + }), ); diff --git a/src/store/states/inbox/actions.ts b/src/store/states/inbox/actions.ts index 77f59adae..3400fe54e 100644 --- a/src/store/states/inbox/actions.ts +++ b/src/store/states/inbox/actions.ts @@ -1,6 +1,6 @@ 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"; @@ -119,3 +119,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 6d5e907ee..d51c47575 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 a0b3fac21..711cbaca2 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<InboxState, Action>(INITIAL_INBOX_STATE) .handleAction(actions.resetInbox, (state, { payload }) => { if (payload?.onlyIfUnread && !state.items.unread) { @@ -775,4 +783,39 @@ export const reducer = createReducer<InboxState, Action>(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 + }); + } + } + }) ); From f9de996179dae5e5cc22cf65019c65addc18a998 Mon Sep 17 00:00:00 2001 From: Pavel Meyer <meyerpavel1207@gmail.com> Date: Wed, 6 Nov 2024 16:24:50 +0300 Subject: [PATCH 2/6] CW-instant reorder Added instant inbox --- .husky/pre-commit | 6 +- .../WalletComponent/hooks.ts | 216 +++++++++--------- .../ChatComponent/ChatComponent.tsx | 5 +- .../NewDiscussionCreation.tsx | 37 ++- .../NewProposalCreation.tsx | 38 +-- .../DiscussionFeedCard/DiscussionFeedCard.tsx | 4 +- src/pages/inbox/BaseInbox.tsx | 10 +- .../hooks/useCases/useCommonFeedItems.ts | 3 +- src/shared/hooks/useCases/useInboxItems.ts | 12 + src/shared/interfaces/State.tsx | 2 + src/store/reducer.tsx | 2 + src/store/states/common/actions.ts | 31 --- src/store/states/common/constants.ts | 9 - src/store/states/common/reducer.ts | 92 +------- src/store/states/common/selectors.ts | 9 - src/store/states/common/types.ts | 4 - src/store/states/index.ts | 1 + src/store/states/optimistic/actions.ts | 42 ++++ src/store/states/optimistic/constants.ts | 11 + src/store/states/optimistic/index.ts | 4 + src/store/states/optimistic/reducer.ts | 133 +++++++++++ src/store/states/optimistic/selectors.ts | 14 ++ src/store/states/optimistic/types.ts | 11 + src/store/store.tsx | 56 ++++- 24 files changed, 452 insertions(+), 300 deletions(-) create mode 100644 src/store/states/optimistic/actions.ts create mode 100644 src/store/states/optimistic/constants.ts create mode 100644 src/store/states/optimistic/index.ts create mode 100644 src/store/states/optimistic/reducer.ts create mode 100644 src/store/states/optimistic/selectors.ts create mode 100644 src/store/states/optimistic/types.ts diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af21989..345a252cc 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/OldCommon/components/CommonDetailContainer/WalletComponent/hooks.ts b/src/pages/OldCommon/components/CommonDetailContainer/WalletComponent/hooks.ts index a6de0ef00..5d2234738 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 b833ff979..93938a5f7 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -66,6 +66,7 @@ import { commonActions, selectOptimisticDiscussionMessages, inboxActions, + optimisticActions, } from "@/store/states"; import { ChatContentContext, ChatContentData } from "../CommonContent/context"; import { @@ -296,7 +297,7 @@ export default function ChatComponent({ }); dispatch( - commonActions.clearOptimisticDiscussionMessages( + optimisticActions.clearOptimisticDiscussionMessages( optimisticMessageDiscussionId, ), ); @@ -574,7 +575,7 @@ export default function ChatComponent({ } if (isOptimisticChat) { - dispatch(commonActions.setOptimisticDiscussionMessages(payload)); + dispatch(optimisticActions.setOptimisticDiscussionMessages(payload)); } else { setMessages((prev) => { if (isFilesMessageWithoutTextAndImages) { 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 e30f9ff74..961955804 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,24 @@ const NewDiscussionCreation: FC<NewDiscussionCreationProps> = (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(optimisticFeedItem)); + 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 ce18e3c25..a708631c0 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,24 @@ const NewProposalCreation: FC<NewProposalCreationProps> = (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(optimisticFeedItem)); + 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 ed66caaa2..61547c201 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; @@ -288,7 +288,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/inbox/BaseInbox.tsx b/src/pages/inbox/BaseInbox.tsx index c0764254e..aac314cf3 100644 --- a/src/pages/inbox/BaseInbox.tsx +++ b/src/pages/inbox/BaseInbox.tsx @@ -37,6 +37,7 @@ import { Loader, NotFound, PureCommonTopNavigation } from "@/shared/ui-kit"; import { inboxActions, selectChatChannelItems, + selectOptimisticInboxFeedItems, selectInboxSearchValue, selectIsSearchingInboxItems, selectNextChatChannelItemId, @@ -106,6 +107,8 @@ const InboxPage: FC<InboxPageProps> = (props) => { const sharedInboxItem = useSelector(selectSharedInboxItem); const chatChannelItems = useSelector(selectChatChannelItems); const nextChatChannelItemId = useSelector(selectNextChatChannelItemId); + const optimisticInboxFeedItems = useSelector(selectOptimisticInboxFeedItems); + const getEmptyText = (): string => { if (hasMoreInboxItems) { @@ -124,6 +127,11 @@ const InboxPage: FC<InboxPageProps> = (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); } @@ -132,7 +140,7 @@ const InboxPage: FC<InboxPageProps> = (props) => { } return items; - }, [chatChannelItems, sharedInboxItem]); + }, [chatChannelItems, sharedInboxItem, optimisticInboxFeedItems]); useUpdateEffect(() => { refetchInboxItems(); diff --git a/src/shared/hooks/useCases/useCommonFeedItems.ts b/src/shared/hooks/useCases/useCommonFeedItems.ts index d527fe5f3..f4f27922e 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 19b5fde24..8da4fd264 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, + FeedItemFollowLayoutItemWithFollowData, FeedLayoutItemWithFollowData, InboxItemsBatch as ItemsBatch, } from "@/shared/interfaces"; @@ -12,8 +13,10 @@ import { InboxItem, Timestamp } from "@/shared/models"; import { inboxActions, InboxItems, + optimisticActions, selectFilteredInboxItems, selectInboxItems, + selectOptimisticInboxFeedItems, } from "@/store/states"; interface Return @@ -67,6 +70,7 @@ export const useInboxItems = ( options?: { unread?: boolean }, ): Return => { const dispatch = useDispatch(); + const optimisticInboxItems = useSelector(selectOptimisticInboxFeedItems); const [newItemsBatches, setNewItemsBatches] = useState<ItemsBatch[]>([]); const [lastUpdatedAt, setLastUpdatedAt] = useState<Timestamp | null>(null); const inboxItems = useSelector(selectInboxItems); @@ -261,6 +265,14 @@ export const useInboxItems = ( if (finalData.length > 0 && isMounted) { dispatch(inboxActions.addNewInboxItems(finalData)); + finalData.forEach(({item}) => { + const itemData = (item as FeedItemFollowLayoutItemWithFollowData).feedItem?.data; + 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})); + } + }) } } catch (error) { Logger.error(error); diff --git a/src/shared/interfaces/State.tsx b/src/shared/interfaces/State.tsx index a1e49a4bb..2adf19a15 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/store/reducer.tsx b/src/store/reducer.tsx index 7d885610b..cf96f039e 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 cedc09af7..ef1848fec 100644 --- a/src/store/states/common/actions.ts +++ b/src/store/states/common/actions.ts @@ -18,7 +18,6 @@ import { Discussion, Governance, LastMessageContentWithMessageId, - OptimisticFeedItemState, Proposal, } from "@/shared/models"; import { CommonActionType } from "./constants"; @@ -29,7 +28,6 @@ import { FeedItemsPayload, PinnedFeedItems, } from "./types"; -import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; export const resetCommon = createStandardAction( CommonActionType.RESET_COMMON, @@ -221,35 +219,6 @@ export const setSharedFeedItem = createStandardAction( CommonActionType.SET_SHARED_FEED_ITEM, )<CommonFeed | null>(); -export const setOptimisticFeedItem = createStandardAction( - CommonActionType.SET_OPTIMISTIC_FEED_ITEM, -)<CommonFeed>(); - -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, -)<CreateDiscussionMessageDto>(); - -export const clearOptimisticDiscussionMessages = createStandardAction( - CommonActionType.CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES, -)<string>(); - -export const clearCreatedOptimisticFeedItem = createStandardAction( - CommonActionType.CLEAR_CREATED_OPTIMISTIC_FEED_ITEM, -)<string>(); - export const setRecentStreamId = createStandardAction( CommonActionType.SET_RECENT_STREAM_ID, )<string>(); diff --git a/src/store/states/common/constants.ts b/src/store/states/common/constants.ts index fb0c82dd4..94a9e725f 100644 --- a/src/store/states/common/constants.ts +++ b/src/store/states/common/constants.ts @@ -56,15 +56,6 @@ 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", diff --git a/src/store/states/common/reducer.ts b/src/store/states/common/reducer.ts index 6bca6584c..578d6d0f6 100644 --- a/src/store/states/common/reducer.ts +++ b/src/store/states/common/reducer.ts @@ -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,93 +693,6 @@ export const reducer = createReducer<CommonState, Action>(initialState) : null; }), ) - .handleAction(actions.setOptimisticFeedItem, (state, { payload }) => - produce(state, (nextState) => { - const updatedMap = new Map(nextState.optimisticFeedItems); - - 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 - - if(optimisticFeedItem && optimisticFeedItem?.feedItem.optimisticData) { - updatedMap.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; - }), - ) - .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; - }), - ) .handleAction(actions.setFeedItemUpdatedAt, (state, { payload }) => produce(state, (nextState) => { const feedItemId = payload.feedItemId; @@ -810,4 +720,4 @@ export const reducer = createReducer<CommonState, Action>(initialState) }); } }), - ); + ); \ No newline at end of file diff --git a/src/store/states/common/selectors.ts b/src/store/states/common/selectors.ts index 541fd87c2..025cf24e4 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 d21b4d50b..0d9eee745 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<T> = { @@ -45,9 +44,6 @@ export interface CommonState { pinnedFeedItems: PinnedFeedItems; sharedFeedItemId: string | null; sharedFeedItem: FeedItemFollowLayoutItem | null; - createdOptimisticFeedItems: Map<string, FeedItemFollowLayoutItem | undefined>; - optimisticFeedItems: Map<string, FeedItemFollowLayoutItem>; - optimisticDiscussionMessages: Map<string, CreateDiscussionMessageDto[]>; commonAction: CommonAction | null; discussionCreation: EntityCreation<NewDiscussionCreationFormValues>; proposalCreation: EntityCreation<NewProposalCreationFormValues>; diff --git a/src/store/states/index.ts b/src/store/states/index.ts index 0cb8d2e2b..3ad06a46b 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 000000000..11c9db5c7 --- /dev/null +++ b/src/store/states/optimistic/actions.ts @@ -0,0 +1,42 @@ +import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; +import { + CommonFeed, + OptimisticFeedItemState +} from "@/shared/models"; +import { createStandardAction } from "typesafe-actions"; +import { OptimisticActionType } from "./constants"; + +export const setOptimisticFeedItem = createStandardAction( + OptimisticActionType.SET_OPTIMISTIC_FEED_ITEM, +)<CommonFeed>(); + +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, +)<CreateDiscussionMessageDto>(); + +export const clearOptimisticDiscussionMessages = createStandardAction( + OptimisticActionType.CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES, +)<string>(); + +export const clearCreatedOptimisticFeedItem = createStandardAction( + OptimisticActionType.CLEAR_CREATED_OPTIMISTIC_FEED_ITEM, +)<string>(); \ 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 000000000..4a1cff0b2 --- /dev/null +++ b/src/store/states/optimistic/constants.ts @@ -0,0 +1,11 @@ +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", + + CLEAR_CREATED_OPTIMISTIC_FEED_ITEM = "@OPTIMISTIC/CLEAR_CREATED_OPTIMISTIC_FEED_ITEM", +} diff --git a/src/store/states/optimistic/index.ts b/src/store/states/optimistic/index.ts new file mode 100644 index 000000000..85d83b51d --- /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 000000000..a6a6049c0 --- /dev/null +++ b/src/store/states/optimistic/reducer.ts @@ -0,0 +1,133 @@ +import { InboxItemType } from "@/shared/constants"; +import produce from "immer"; +import { ActionType, createReducer } from "typesafe-actions"; +import * as actions from "./actions"; +import { + OptimisticState +} from "./types"; + +type Action = ActionType<typeof actions>; + +const initialState: OptimisticState = { + optimisticFeedItems: new Map(), + optimisticInboxFeedItems: new Map(), + optimisticDiscussionMessages: new Map(), + createdOptimisticFeedItems: new Map(), +}; + +export const reducer = createReducer<OptimisticState, Action>(initialState) + .handleAction(actions.setOptimisticFeedItem, (state, { payload }) => + produce(state, (nextState) => { + const updatedMap = new Map(nextState.optimisticFeedItems); + const updateMapInbox = new Map(nextState.optimisticInboxFeedItems); + + 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, + }); + updateMapInbox.set(optimisticItemId, { + type: InboxItemType.FeedItemFollow, + itemId: payload.id, + feedItem: payload, + }) + + // 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 000000000..6bedea23d --- /dev/null +++ b/src/store/states/optimistic/selectors.ts @@ -0,0 +1,14 @@ +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; diff --git a/src/store/states/optimistic/types.ts b/src/store/states/optimistic/types.ts new file mode 100644 index 000000000..4a24fa8c3 --- /dev/null +++ b/src/store/states/optimistic/types.ts @@ -0,0 +1,11 @@ +import { + FeedItemFollowLayoutItem +} from "@/shared/interfaces"; +import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; + +export interface OptimisticState { + createdOptimisticFeedItems: Map<string, FeedItemFollowLayoutItem | undefined>; + optimisticFeedItems: Map<string, FeedItemFollowLayoutItem>; + optimisticInboxFeedItems: Map<string, FeedItemFollowLayoutItem>; + optimisticDiscussionMessages: Map<string, CreateDiscussionMessageDto[]>; +} diff --git a/src/store/store.tsx b/src/store/store.tsx index b56c38a91..623c46b39 100644 --- a/src/store/store.tsx +++ b/src/store/store.tsx @@ -23,11 +23,65 @@ 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<keyof OptimisticState> = [ + "createdOptimisticFeedItems", + "optimisticFeedItems", + "optimisticInboxFeedItems", + "optimisticDiscussionMessages" +]; + +const mapTransformer = createTransform<OptimisticState, OptimisticState>( + (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<string, CreateDiscussionMessageDto[]>).entries() + ) as [string, CreateDiscussionMessageDto[]][]; + } else { + (transformedState as any)[key] = Array.from( + (value as Map<string, FeedItemFollowLayoutItem | undefined>).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<string, CreateDiscussionMessageDto[]>; + } else { + (transformedState as any)[key] = new Map(value) as Map<string, FeedItemFollowLayoutItem | undefined>; + } + } + }); + + return transformedState; + }, + { whitelist: ["optimistic"] } +); + const persistConfig: PersistConfig<AppState> = { key: "root", storage, whitelist: [ + "optimistic", "projects", "commonLayout", "commonFeedFollows", @@ -36,7 +90,7 @@ const persistConfig: PersistConfig<AppState> = { "multipleSpacesLayout", ], stateReconciler: autoMergeLevel2, - transforms: [inboxTransform, cacheTransform, multipleSpacesLayoutTransform], + transforms: [mapTransformer, inboxTransform, cacheTransform, multipleSpacesLayoutTransform], }; const sagaMiddleware = createSagaMiddleware(); From 86759f2a5a9925b4092bc6ed6c34b7724df3bec0 Mon Sep 17 00:00:00 2001 From: Pavel Meyer <meyerpavel1207@gmail.com> Date: Thu, 7 Nov 2024 14:49:46 +0300 Subject: [PATCH 3/6] CW-instant-reorder Fix Inbox items Fix inbox focus Added flush for persist store after logout --- src/pages/Auth/store/saga.tsx | 19 ++++++++++--- .../NewDiscussionCreation.tsx | 5 +++- .../NewProposalCreation.tsx | 5 +++- .../components/FeedLayout/FeedLayout.tsx | 1 + src/pages/inbox/BaseInbox.tsx | 13 +++++++++ src/shared/hooks/useCases/useInboxItems.ts | 1 + .../utils/generateOptimisticFeedItem.ts | 27 ++++++++++++++++++- src/store/states/optimistic/actions.ts | 12 +++++++-- src/store/states/optimistic/constants.ts | 1 + src/store/states/optimistic/reducer.ts | 16 ++++++----- 10 files changed, 85 insertions(+), 15 deletions(-) diff --git a/src/pages/Auth/store/saga.tsx b/src/pages/Auth/store/saga.tsx index a3240b05d..a73fb31f7 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/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/NewDiscussionCreation.tsx index 961955804..d70e1ee08 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 @@ -139,7 +139,10 @@ const NewDiscussionCreation: FC<NewDiscussionCreationProps> = (props) => { content: generateFirstMessage({userName, userId}), } }); - dispatch(optimisticActions.setOptimisticFeedItem(optimisticFeedItem)); + dispatch(optimisticActions.setOptimisticFeedItem({ + data: optimisticFeedItem, + common + })); dispatch(commonActions.setRecentStreamId(optimisticFeedItem.data.id)); dispatch( commonActions.createDiscussion.request({ 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 a708631c0..06ba04f3a 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 @@ -107,7 +107,10 @@ const NewProposalCreation: FC<NewProposalCreationProps> = (props) => { } }); - dispatch(optimisticActions.setOptimisticFeedItem(optimisticFeedItem)); + dispatch(optimisticActions.setOptimisticFeedItem({ + data: optimisticFeedItem, + common + })); dispatch(commonActions.setRecentStreamId(optimisticFeedItem.data.id)); switch (values.proposalType.value) { case ProposalsTypes.FUNDS_ALLOCATION: { diff --git a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx index 64ba26978..4bfc9ef35 100644 --- a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx +++ b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx @@ -852,6 +852,7 @@ const FeedLayout: ForwardRefRenderFunction<FeedLayoutRef, FeedLayoutProps> = ( item.feedItemFollowWithMetadata, outerCommon, ); + const isPinned = ( outerCommon?.pinnedFeedItems || [] ).some( diff --git a/src/pages/inbox/BaseInbox.tsx b/src/pages/inbox/BaseInbox.tsx index aac314cf3..343824593 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, @@ -146,6 +147,18 @@ const InboxPage: FC<InboxPageProps> = (props) => { 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, diff --git a/src/shared/hooks/useCases/useInboxItems.ts b/src/shared/hooks/useCases/useInboxItems.ts index 8da4fd264..818f69db7 100644 --- a/src/shared/hooks/useCases/useInboxItems.ts +++ b/src/shared/hooks/useCases/useInboxItems.ts @@ -267,6 +267,7 @@ export const useInboxItems = ( dispatch(inboxActions.addNewInboxItems(finalData)); finalData.forEach(({item}) => { const itemData = (item as FeedItemFollowLayoutItemWithFollowData).feedItem?.data; + if(optimisticInboxItems.has(itemData.id)) { dispatch(optimisticActions.removeOptimisticInboxFeedItemState({id: itemData.id})); } else if (itemData?.discussionId && optimisticInboxItems.has(itemData?.discussionId)) { diff --git a/src/shared/utils/generateOptimisticFeedItem.ts b/src/shared/utils/generateOptimisticFeedItem.ts index df9731e3d..18ce7a2e5 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/states/optimistic/actions.ts b/src/store/states/optimistic/actions.ts index 11c9db5c7..5c741c01d 100644 --- a/src/store/states/optimistic/actions.ts +++ b/src/store/states/optimistic/actions.ts @@ -1,5 +1,6 @@ import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; import { + Common, CommonFeed, OptimisticFeedItemState } from "@/shared/models"; @@ -8,7 +9,10 @@ import { OptimisticActionType } from "./constants"; export const setOptimisticFeedItem = createStandardAction( OptimisticActionType.SET_OPTIMISTIC_FEED_ITEM, -)<CommonFeed>(); +)<{ + data: CommonFeed; + common: Common; +}>(); export const updateOptimisticFeedItemState = createStandardAction( OptimisticActionType.UPDATE_OPTIMISTIC_FEED_ITEM, @@ -39,4 +43,8 @@ export const clearOptimisticDiscussionMessages = createStandardAction( export const clearCreatedOptimisticFeedItem = createStandardAction( OptimisticActionType.CLEAR_CREATED_OPTIMISTIC_FEED_ITEM, -)<string>(); \ No newline at end of file +)<string>(); + +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 index 4a1cff0b2..78d33e072 100644 --- a/src/store/states/optimistic/constants.ts +++ b/src/store/states/optimistic/constants.ts @@ -8,4 +8,5 @@ export enum OptimisticActionType { CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES = "@OPTIMISTIC/CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES", CLEAR_CREATED_OPTIMISTIC_FEED_ITEM = "@OPTIMISTIC/CLEAR_CREATED_OPTIMISTIC_FEED_ITEM", + RESET_OPTIMISTIC_STATE = "RESET_OPTIMISTIC_STATE", } diff --git a/src/store/states/optimistic/reducer.ts b/src/store/states/optimistic/reducer.ts index a6a6049c0..b29d452c2 100644 --- a/src/store/states/optimistic/reducer.ts +++ b/src/store/states/optimistic/reducer.ts @@ -5,6 +5,7 @@ import * as actions from "./actions"; import { OptimisticState } from "./types"; +import { generateOptimisticFeedItemFollowWithMetadata } from "@/shared/utils"; type Action = ActionType<typeof actions>; @@ -16,24 +17,25 @@ const initialState: OptimisticState = { }; export const reducer = createReducer<OptimisticState, Action>(initialState) + .handleAction(actions.resetOptimisticState, () => initialState) .handleAction(actions.setOptimisticFeedItem, (state, { payload }) => produce(state, (nextState) => { const updatedMap = new Map(nextState.optimisticFeedItems); const updateMapInbox = new Map(nextState.optimisticInboxFeedItems); - const optimisticItemId = payload.data.discussionId ?? payload.data.id; + 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.id, - feedItem: payload, + itemId: payload.data.id, + feedItem: payload.data, }); updateMapInbox.set(optimisticItemId, { type: InboxItemType.FeedItemFollow, - itemId: payload.id, - feedItem: payload, - }) - + 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; From 710cd9cf5ed66ff3d8557da50d5f21821abefb18 Mon Sep 17 00:00:00 2001 From: Pavel Meyer <meyerpavel1207@gmail.com> Date: Thu, 7 Nov 2024 14:58:32 +0300 Subject: [PATCH 4/6] CW-instant-reorder Added scrolling behaviour --- src/pages/common/components/ChatComponent/ChatComponent.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 93938a5f7..8e7f1570e 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -622,6 +622,9 @@ export default function ChatComponent({ dispatch(commonActions.setFeedItemUpdatedAt(payloadUpdateFeedItem)); dispatch(inboxActions.setInboxItemUpdatedAt(payloadUpdateFeedItem)); + document + .getElementById("feedLayoutWrapper") + ?.scrollIntoView({ behavior: "smooth" }); focusOnChat(); } }, From cc29ff29ae96a57c8edfe2732d378b60e209cd8d Mon Sep 17 00:00:00 2001 From: Pavel Meyer <meyerpavel1207@gmail.com> Date: Sat, 16 Nov 2024 19:17:52 +0300 Subject: [PATCH 5/6] CW-instant-reorder Added logic for inbox reorder --- .../ChatComponent/ChatComponent.tsx | 10 +++- .../components/FeedCard/FeedCard.module.scss | 4 ++ .../common/components/FeedCard/FeedCard.tsx | 4 +- src/pages/inbox/BaseInbox.tsx | 29 ++++++----- src/shared/hooks/useCases/useInboxItems.ts | 52 +++++++++++++++++-- src/store/states/inbox/actions.ts | 12 +---- src/store/states/inbox/types.ts | 9 ++++ src/store/states/optimistic/actions.ts | 10 ++++ src/store/states/optimistic/constants.ts | 3 ++ src/store/states/optimistic/reducer.ts | 44 ++++++++++++++++ src/store/states/optimistic/selectors.ts | 3 ++ src/store/states/optimistic/types.ts | 5 ++ src/store/store.tsx | 3 +- 13 files changed, 156 insertions(+), 32 deletions(-) diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 8e7f1570e..99b2bec6b 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -67,6 +67,7 @@ import { selectOptimisticDiscussionMessages, inboxActions, optimisticActions, + selectInstantDiscussionMessagesOrder, } from "@/store/states"; import { ChatContentContext, ChatContentData } from "../CommonContent/context"; import { @@ -89,6 +90,7 @@ import styles from "./ChatComponent.module.scss"; import { BaseTextEditorHandles } from "@/shared/ui-kit/TextEditor/BaseTextEditor"; const BASE_CHAT_INPUT_HEIGHT = 48; +const BASE_ORDER_INTERVAL = 1000; interface ChatComponentInterface { commonId: string; @@ -277,6 +279,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); @@ -416,8 +421,8 @@ export default function ChatComponent({ setMessages([]); } }, - 1500, - [newMessages, discussionId, dispatch], + 1500 + BASE_ORDER_INTERVAL * currentChatOrder, + [newMessages, discussionId, dispatch, currentChatOrder], ); /** @@ -584,6 +589,7 @@ export default function ChatComponent({ return [...prev, ...filePreviewPayload, payload]; }); + dispatch(optimisticActions.setInstantDiscussionMessagesOrder({discussionId})); } if (isChatChannel) { diff --git a/src/pages/common/components/FeedCard/FeedCard.module.scss b/src/pages/common/components/FeedCard/FeedCard.module.scss index 7deb66dbe..8ff002e12 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 28860dc8a..1b1625819 100644 --- a/src/pages/common/components/FeedCard/FeedCard.tsx +++ b/src/pages/common/components/FeedCard/FeedCard.tsx @@ -299,8 +299,8 @@ const FeedCard = (props, ref) => { ]); return ( - <div ref={containerRef}> - {!isPreviewMode && <div {...getToggleProps()}>{feedItemBaseContent}</div>} + <div ref={containerRef} > + {!isPreviewMode && <div className={styles.toggleCard} {...getToggleProps()}>{feedItemBaseContent}</div>} <div {...getCollapseProps()}> <CommonCard className={classNames( diff --git a/src/pages/inbox/BaseInbox.tsx b/src/pages/inbox/BaseInbox.tsx index 343824593..711ba2868 100644 --- a/src/pages/inbox/BaseInbox.tsx +++ b/src/pages/inbox/BaseInbox.tsx @@ -43,6 +43,7 @@ import { selectIsSearchingInboxItems, selectNextChatChannelItemId, selectSharedInboxItem, + selectInstantDiscussionMessagesOrder, } from "@/store/states"; import { ChatChannelItem, FeedItemBaseContent } from "./components"; import { useInboxData } from "./hooks"; @@ -181,22 +182,26 @@ const InboxPage: FC<InboxPageProps> = (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/useCases/useInboxItems.ts b/src/shared/hooks/useCases/useInboxItems.ts index 818f69db7..e9fefd046 100644 --- a/src/shared/hooks/useCases/useInboxItems.ts +++ b/src/shared/hooks/useCases/useInboxItems.ts @@ -7,17 +7,21 @@ 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<InboxItems, "data" | "loading" | "hasMore" | "batchNumber"> { @@ -71,6 +75,9 @@ export const useInboxItems = ( ): Return => { const dispatch = useDispatch(); const optimisticInboxItems = useSelector(selectOptimisticInboxFeedItems); + const instantDiscussionMessages = useSelector( + selectInstantDiscussionMessagesOrder, + ); const [newItemsBatches, setNewItemsBatches] = useState<ItemsBatch[]>([]); const [lastUpdatedAt, setLastUpdatedAt] = useState<Timestamp | null>(null); const inboxItems = useSelector(selectInboxItems); @@ -244,6 +251,33 @@ export const useInboxItems = ( unread, ]); + const [notListedFeedItems, setNotListedFeedItems] = useState<InboxItemBatch<FeedLayoutItemWithFollowData>[]>([]); + + 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; @@ -264,16 +298,23 @@ export const useInboxItems = ( ); if (finalData.length > 0 && isMounted) { - dispatch(inboxActions.addNewInboxItems(finalData)); - finalData.forEach(({item}) => { - const itemData = (item as FeedItemFollowLayoutItemWithFollowData).feedItem?.data; - + const newItems: InboxItemBatch<FeedLayoutItemWithFollowData>[] = []; + finalData.forEach((item: InboxItemBatch<FeedLayoutItemWithFollowData>) => { + 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); @@ -285,7 +326,8 @@ export const useInboxItems = ( return () => { isMounted = false; }; - }, [lastBatch]); + }, [lastBatch, instantDiscussionMessages]); + return { ...inboxItems, diff --git a/src/store/states/inbox/actions.ts b/src/store/states/inbox/actions.ts index 3400fe54e..004ad09be 100644 --- a/src/store/states/inbox/actions.ts +++ b/src/store/states/inbox/actions.ts @@ -2,7 +2,7 @@ import { createAsyncAction, createStandardAction } from "typesafe-actions"; import { FeedLayoutItemWithFollowData } from "@/shared/interfaces"; 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; - }; - }[] ->(); +)<NewInboxItems[]>(); export const updateInboxItem = createStandardAction( InboxActionType.UPDATE_INBOX_ITEM, diff --git a/src/store/states/inbox/types.ts b/src/store/states/inbox/types.ts index 58542ac2a..c18c75a67 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/optimistic/actions.ts b/src/store/states/optimistic/actions.ts index 5c741c01d..4a94826bb 100644 --- a/src/store/states/optimistic/actions.ts +++ b/src/store/states/optimistic/actions.ts @@ -41,6 +41,16 @@ export const clearOptimisticDiscussionMessages = createStandardAction( OptimisticActionType.CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES, )<string>(); +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, )<string>(); diff --git a/src/store/states/optimistic/constants.ts b/src/store/states/optimistic/constants.ts index 78d33e072..9bca26b87 100644 --- a/src/store/states/optimistic/constants.ts +++ b/src/store/states/optimistic/constants.ts @@ -7,6 +7,9 @@ export enum OptimisticActionType { 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/reducer.ts b/src/store/states/optimistic/reducer.ts index b29d452c2..a437af2cd 100644 --- a/src/store/states/optimistic/reducer.ts +++ b/src/store/states/optimistic/reducer.ts @@ -6,6 +6,7 @@ import { OptimisticState } from "./types"; import { generateOptimisticFeedItemFollowWithMetadata } from "@/shared/utils"; +import { Timestamp } from "@/shared/models"; type Action = ActionType<typeof actions>; @@ -14,10 +15,53 @@ const initialState: OptimisticState = { optimisticInboxFeedItems: new Map(), optimisticDiscussionMessages: new Map(), createdOptimisticFeedItems: new Map(), + instantDiscussionMessagesOrder: new Map(), }; export const reducer = createReducer<OptimisticState, Action>(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); diff --git a/src/store/states/optimistic/selectors.ts b/src/store/states/optimistic/selectors.ts index 6bedea23d..bed310b5e 100644 --- a/src/store/states/optimistic/selectors.ts +++ b/src/store/states/optimistic/selectors.ts @@ -12,3 +12,6 @@ export const selectOptimisticDiscussionMessages = (state: AppState) => 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 index 4a24fa8c3..fe11016df 100644 --- a/src/store/states/optimistic/types.ts +++ b/src/store/states/optimistic/types.ts @@ -2,10 +2,15 @@ import { FeedItemFollowLayoutItem } from "@/shared/interfaces"; import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages"; +import { Timestamp } from "@/shared/models"; export interface OptimisticState { createdOptimisticFeedItems: Map<string, FeedItemFollowLayoutItem | undefined>; optimisticFeedItems: Map<string, FeedItemFollowLayoutItem>; optimisticInboxFeedItems: Map<string, FeedItemFollowLayoutItem>; optimisticDiscussionMessages: Map<string, CreateDiscussionMessageDto[]>; + instantDiscussionMessagesOrder: Map<string, { + order: number + timestamp: Timestamp; + }>; } diff --git a/src/store/store.tsx b/src/store/store.tsx index 623c46b39..915483972 100644 --- a/src/store/store.tsx +++ b/src/store/store.tsx @@ -33,7 +33,8 @@ const mapFields: Array<keyof OptimisticState> = [ "createdOptimisticFeedItems", "optimisticFeedItems", "optimisticInboxFeedItems", - "optimisticDiscussionMessages" + "optimisticDiscussionMessages", + "instantDiscussionMessagesOrder" ]; const mapTransformer = createTransform<OptimisticState, OptimisticState>( From 9f6475816db64e3333373d7c7775d42742c651b6 Mon Sep 17 00:00:00 2001 From: Pavel Meyer <meyerpavel1207@gmail.com> Date: Sun, 17 Nov 2024 01:21:09 +0300 Subject: [PATCH 6/6] CW-instant-reorder Added feedItem keyboard support --- .../ChatComponent/ChatComponent.tsx | 15 ++++++ .../DiscussionFeedCard/DiscussionFeedCard.tsx | 6 --- .../common/components/FeedCard/FeedCard.tsx | 2 +- .../common/components/FeedItem/context.ts | 1 + .../components/FeedLayout/FeedLayout.tsx | 53 ++++++++++++++++++- .../components/FeedLayout/constants/index.ts | 4 ++ src/shared/hooks/index.tsx | 2 + src/shared/hooks/useElementPresence.tsx | 48 +++++++++++++++++ src/shared/hooks/useIsElementFocused.tsx | 24 +++++++++ .../utils/countTextEditorEmojiElements.ts | 1 + 10 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 src/shared/hooks/useElementPresence.tsx create mode 100644 src/shared/hooks/useIsElementFocused.tsx diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 99b2bec6b..c7350bd0b 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -88,6 +88,7 @@ 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; @@ -258,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], diff --git a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx index 61547c201..dcd82a373 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -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({ diff --git a/src/pages/common/components/FeedCard/FeedCard.tsx b/src/pages/common/components/FeedCard/FeedCard.tsx index 1b1625819..5fac7dcb5 100644 --- a/src/pages/common/components/FeedCard/FeedCard.tsx +++ b/src/pages/common/components/FeedCard/FeedCard.tsx @@ -299,7 +299,7 @@ const FeedCard = (props, ref) => { ]); return ( - <div ref={containerRef} > + <div ref={containerRef}> {!isPreviewMode && <div className={styles.toggleCard} {...getToggleProps()}>{feedItemBaseContent}</div>} <div {...getCollapseProps()}> <CommonCard diff --git a/src/pages/common/components/FeedItem/context.ts b/src/pages/common/components/FeedItem/context.ts index e6cb31770..22c10d418 100644 --- a/src/pages/common/components/FeedItem/context.ts +++ b/src/pages/common/components/FeedItem/context.ts @@ -71,6 +71,7 @@ export interface GetLastMessageOptions { } export interface FeedItemContextValue { + setIsInputFocused?: (isFocused: boolean) => 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 4bfc9ef35..91f55aafe 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<FeedLayoutRef, FeedLayoutProps> = ( 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<FeedLayoutRef, FeedLayoutProps> = ( 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<ChatContextValue>( () => ({ setChatItem: setActiveChatItem, @@ -644,6 +691,7 @@ const FeedLayout: ForwardRefRenderFunction<FeedLayoutRef, FeedLayoutProps> = ( // so we will not have extra re-renders of ALL rendered items const feedItemContextValue = useMemo<FeedItemContextValue>( () => ({ + setIsInputFocused, setExpandedFeedItemId, renderFeedItemBaseContent, onFeedItemUpdate, @@ -656,6 +704,7 @@ const FeedLayout: ForwardRefRenderFunction<FeedLayoutRef, FeedLayoutProps> = ( onActiveItemDataChange: handleActiveFeedItemDataChange, }), [ + setIsInputFocused, renderFeedItemBaseContent, onFeedItemUpdate, onFeedItemUnfollowed, diff --git a/src/pages/commonFeed/components/FeedLayout/constants/index.ts b/src/pages/commonFeed/components/FeedLayout/constants/index.ts index 68c376375..b64b9cd95 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/shared/hooks/index.tsx b/src/shared/hooks/index.tsx index 7c78ecdd5..5371485a1 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/useElementPresence.tsx b/src/shared/hooks/useElementPresence.tsx new file mode 100644 index 000000000..27eb4f74c --- /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 000000000..da6dd02e9 --- /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/ui-kit/TextEditor/utils/countTextEditorEmojiElements.ts b/src/shared/ui-kit/TextEditor/utils/countTextEditorEmojiElements.ts index f8bdcd9ec..828decab8 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 &&