diff --git a/src/index.tsx b/src/index.tsx index d5cdd816d6..e41e020189 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,8 +1,11 @@ import "./projectSetupImports"; import React from "react"; import ReactDOM from "react-dom"; +import { enableMapSet } from "immer"; import { App, AppWrapper } from "@/pages/App"; +enableMapSet(); + ReactDOM.render( diff --git a/src/pages/OldCommon/components/CommonDetailContainer/AddDiscussionComponent/AddDiscussionComponent.tsx b/src/pages/OldCommon/components/CommonDetailContainer/AddDiscussionComponent/AddDiscussionComponent.tsx index 7037f4c722..465f93f95f 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/AddDiscussionComponent/AddDiscussionComponent.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/AddDiscussionComponent/AddDiscussionComponent.tsx @@ -5,6 +5,7 @@ import classNames from "classnames"; import { Formik } from "formik"; import { omit } from "lodash"; import * as Yup from "yup"; +import { v4 as uuidv4 } from "uuid"; import { createDiscussion } from "@/pages/OldCommon/store/actions"; import { getCommonGovernanceCircles } from "@/pages/OldCommon/store/api"; import { Modal } from "@/shared/components"; @@ -109,10 +110,14 @@ const AddDiscussionComponent = ({ ); const payload = omit(values, "isLimitedDiscussion"); + + // TODO: CHECK if it needed for optimistic + const discussionId = uuidv4(); dispatch( createDiscussion.request({ payload: { ...payload, + id: discussionId, ownerId: uid, commonId: commonId, circleVisibility, diff --git a/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalComponent.tsx b/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalComponent.tsx index 4f842c14d3..1336f47986 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalComponent.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalComponent.tsx @@ -6,6 +6,7 @@ import React, { useState, } from "react"; import { useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import classNames from "classnames"; import { Modal } from "@/shared/components"; import { AllocateFundsTo, ScreenSize } from "@/shared/constants"; @@ -67,6 +68,8 @@ export const AddProposalComponent = ({ const [fundingRequest, setFundingRequest] = useState({ args: { + id: "", + discussionId: "", title: "", description: "", links: [], @@ -125,8 +128,10 @@ export const AddProposalComponent = ({ const saveProposalState = useCallback( (payload: Partial) => { + const proposalId = uuidv4(); + const discussionId = uuidv4(); const fundingRequestData = { - args: { ...fundingRequest.args, ...payload }, + args: { ...fundingRequest.args, ...payload, id: proposalId, discussionId }, }; setFundingRequest(fundingRequestData); if (!payload?.amount) { diff --git a/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalForm.tsx b/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalForm.tsx index 1034da0162..ab3029f59e 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalForm.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/AddProposalComponent/AddProposalForm.tsx @@ -3,6 +3,7 @@ import { useDispatch } from "react-redux"; import classNames from "classnames"; import { Formik } from "formik"; import * as Yup from "yup"; +import { v4 as uuidv4 } from "uuid"; import { getBankDetails } from "@/pages/OldCommon/store/actions"; import { Button, ButtonIcon, Loader, ModalFooter } from "@/shared/components"; import { @@ -101,6 +102,8 @@ export const AddProposalForm = ({ }, [dispatch, hidden]); const [formValues] = useState({ + id: "", + discussionId: "", title: "", description: "", links: [{ title: "", value: "" }], @@ -168,7 +171,9 @@ export const AddProposalForm = ({ validationSchema={schema} onSubmit={(values, { setSubmitting }) => { setSubmitting(false); - saveProposalState({ ...values, images: uploadedFiles }); + const proposalId = uuidv4(); + const discussionId = uuidv4(); + saveProposalState({ ...values, images: uploadedFiles, id: proposalId, discussionId }); }} initialValues={formValues} validateOnChange={true} diff --git a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/AssignCircleStage/AssignCircleStage.tsx b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/AssignCircleStage/AssignCircleStage.tsx index cd71256a31..5861938c45 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/AssignCircleStage/AssignCircleStage.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/AssignCircleStage/AssignCircleStage.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { useCommonMembers } from "@/pages/OldCommon/hooks"; import { CreateProposal } from "@/pages/OldCommon/interfaces"; @@ -79,11 +80,15 @@ const AssignCircleStage: FC = (props) => { } setIsProposalCreating(true); + const proposalId = uuidv4(); + const discussionId = uuidv4(); const payload: Omit< CreateProposal[ProposalsTypes.ASSIGN_CIRCLE]["data"], "type" > = { args: { + id: proposalId, + discussionId, commonId: common.id, // TODO: Use here name of common member title: `Request to join ${ diff --git a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/DeleteCommonStage/DeleteCommonStage.tsx b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/DeleteCommonStage/DeleteCommonStage.tsx index f5251cde2b..145aa579af 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/DeleteCommonStage/DeleteCommonStage.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/DeleteCommonStage/DeleteCommonStage.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { CreateProposal } from "@/pages/OldCommon/interfaces"; import { createDeleteCommonProposal } from "@/pages/OldCommon/store/actions"; @@ -68,11 +69,15 @@ const DeleteCommonStage: FC = (props) => { } setIsProposalCreating(true); + const proposalId = uuidv4(); + const discussionId = uuidv4(); const payload: Omit< CreateProposal[ProposalsTypes.DELETE_COMMON]["data"], "type" > = { args: { + id: proposalId, + discussionId, commonId: common.id, title: `Delete common proposal from ${getUserName(user)}`, description: deleteCommonData.description, diff --git a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/FundsAllocationStage/FundsAllocationStage.tsx b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/FundsAllocationStage/FundsAllocationStage.tsx index 5843b5e3d0..6c893cbe42 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/FundsAllocationStage/FundsAllocationStage.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/FundsAllocationStage/FundsAllocationStage.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { useCommonMembers } from "@/pages/OldCommon/hooks"; import { CreateProposal } from "@/pages/OldCommon/interfaces"; import { @@ -133,12 +134,16 @@ const FundsAllocationStage: FC = (props) => { : { otherMemberId: fundsAllocationData.otherMemberId }; setIsProposalCreating(true); + const proposalId = uuidv4(); + const discussionId = uuidv4(); const description = `${fundsAllocationData.description}\n\nGoal of Payment:\n${fundsAllocationData.goalOfPayment}`; const payload: Omit< CreateProposal[ProposalsTypes.FUNDS_ALLOCATION]["data"], "type" > = { args: { + id: proposalId, + discussionId, description, amount: { amount: fundsAllocationData.amount * 100, diff --git a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/RemoveCircleStage/RemoveCircleStage.tsx b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/RemoveCircleStage/RemoveCircleStage.tsx index 9dd62f4b8e..a379ed02df 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/RemoveCircleStage/RemoveCircleStage.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/RemoveCircleStage/RemoveCircleStage.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { useCommonMembers } from "@/pages/OldCommon/hooks"; import { CreateProposal } from "@/pages/OldCommon/interfaces"; @@ -74,11 +75,15 @@ const RemoveCircleStage: FC = (props) => { } setIsProposalCreating(true); + const proposalId = uuidv4(); + const discussionId = uuidv4(); const payload: Omit< CreateProposal[ProposalsTypes.REMOVE_CIRCLE]["data"], "type" > = { args: { + id: proposalId, + discussionId, commonId: common.id, title: `Remove circle proposal for ${getUserName( removeCircleData.commonMember.user, diff --git a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/SurveyStage/SurveyStage.tsx b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/SurveyStage/SurveyStage.tsx index 3b7bfa2dd6..76b7652a7d 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/SurveyStage/SurveyStage.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/CreateProposalModal/SurveyStage/SurveyStage.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { CreateProposal } from "@/pages/OldCommon/interfaces"; import { createSurvey } from "@/pages/OldCommon/store/actions"; import { Loader, Modal } from "@/shared/components"; @@ -68,9 +69,14 @@ const SurveyStage: FC = (props) => { } setIsProposalCreating(true); + + const proposalId = uuidv4(); + const discussionId = uuidv4(); const payload: Omit = { args: { + id: proposalId, + discussionId, description: surveyData.description, commonId: common.id, title: surveyData.title, diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestCreating.tsx b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestCreating.tsx index 945356a834..be4c456f23 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestCreating.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestCreating.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { Loader } from "@/shared/components"; import { ContributionSourceType, Currency } from "@/shared/models"; @@ -39,10 +40,14 @@ export default function MembershipRequestCreating( return; } + const proposalId = uuidv4(); + const discussionId = uuidv4(); dispatch( createMemberAdmittanceProposal.request({ payload: { args: { + id: proposalId, + discussionId, commonId: common.id, title: `Membership request from ${userName}`, description: userData.intro, diff --git a/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceForProjectStep/MemberAdmittanceForProjectStep.tsx b/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceForProjectStep/MemberAdmittanceForProjectStep.tsx index 7abf6cdf6a..2904635115 100644 --- a/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceForProjectStep/MemberAdmittanceForProjectStep.tsx +++ b/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceForProjectStep/MemberAdmittanceForProjectStep.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { DeadSeaUserDetailsFormValuesWithoutUserDetails } from "@/pages/OldCommon/components"; import { useSupportersDataContext } from "@/pages/OldCommon/containers/SupportersContainer/context"; @@ -106,10 +107,14 @@ const MemberAdmittanceForProjectStep: FC< const title = `Membership request from ${userName}`; + const proposalId = uuidv4(); + const discussionId = uuidv4(); dispatch( createMemberAdmittanceProposal.request({ payload: { args: { + id: proposalId, + discussionId, commonId: parentCommonId, title, description: data.supportPlan || title, @@ -180,6 +185,8 @@ const MemberAdmittanceForProjectStep: FC< return; } + const proposalId = uuidv4(); + const discussionId = uuidv4(); try { const title = `${userName} joins and supports ${circleName}`; const payload: Omit< @@ -187,6 +194,8 @@ const MemberAdmittanceForProjectStep: FC< "type" > = { args: { + id: proposalId, + discussionId, commonId: parentCommonId, title, description: data.supportPlan || title, diff --git a/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceStep/MemberAdmittanceStep.tsx b/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceStep/MemberAdmittanceStep.tsx index 6bffe509ea..0fee7adbfd 100644 --- a/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceStep/MemberAdmittanceStep.tsx +++ b/src/pages/OldCommon/components/SupportersContainer/MemberAdmittanceStep/MemberAdmittanceStep.tsx @@ -1,5 +1,6 @@ import React, { FC, useEffect, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { DeadSeaUserDetailsFormValuesWithoutUserDetails } from "@/pages/OldCommon/components"; import { useSupportersDataContext } from "@/pages/OldCommon/containers/SupportersContainer/context"; @@ -64,10 +65,14 @@ const MemberAdmittanceStep: FC = (props) => { const title = `Membership request from ${userName}`; + const proposalId = uuidv4(); + const discussionId = uuidv4(); dispatch( createMemberAdmittanceProposal.request({ payload: { args: { + id: proposalId, + discussionId, commonId, title, description: data.supportPlan || title, diff --git a/src/pages/OldCommon/interfaces/CreateDiscussionDto.tsx b/src/pages/OldCommon/interfaces/CreateDiscussionDto.tsx index 7785ed7aa0..0c4bdd4da7 100644 --- a/src/pages/OldCommon/interfaces/CreateDiscussionDto.tsx +++ b/src/pages/OldCommon/interfaces/CreateDiscussionDto.tsx @@ -1,6 +1,7 @@ import { CommonLink } from "@/shared/models"; export interface CreateDiscussionDto { + id: string; title: string; message: string; ownerId: string; diff --git a/src/pages/OldCommon/store/saga.tsx b/src/pages/OldCommon/store/saga.tsx index feb6a14dee..3554dff7cb 100644 --- a/src/pages/OldCommon/store/saga.tsx +++ b/src/pages/OldCommon/store/saga.tsx @@ -555,7 +555,7 @@ export function* createDiscussionSaga( ); } - yield put(startLoading()); + // yield put(startLoading()); const discussion = (yield createDiscussion( action.payload.payload, diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index de1e7796fd..925bb707c1 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -6,15 +6,16 @@ import React, { ChangeEvent, useRef, ReactNode, + useLayoutEffect, } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useDebounce, useMeasure, useScroll } from "react-use"; import classNames from "classnames"; import isHotkey from "is-hotkey"; -import { debounce, delay, omit } from "lodash"; +import { debounce } from "lodash"; import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; -import { ChatService, DiscussionMessageService, FileService } from "@/services"; +import { FileService } from "@/services"; import { Separator } from "@/shared/components"; import { ChatType, @@ -57,11 +58,13 @@ import { checkUncheckedItemsInTextEditorValue } from "@/shared/ui-kit/TextEditor import { InternalLinkData, notEmpty } from "@/shared/utils"; import { getUserName, hasPermission, isMobile } from "@/shared/utils"; import { - cacheActions, chatActions, selectCurrentDiscussionMessageReply, selectFilesPreview, FileInfo, + selectOptimisticFeedItems, + commonActions, + selectOptimisticDiscussionMessages, } from "@/store/states"; import { ChatContentContext, ChatContentData } from "../CommonContent/context"; import { @@ -75,8 +78,13 @@ import { } from "./components"; import { checkIsLastSeenInPreviousDay } from "./components/ChatContent/utils"; import { useChatChannelChatAdapter, useDiscussionChatAdapter } from "./hooks"; -import { getLastNonUserMessage } from "./utils"; +import { + getLastNonUserMessage, + sendMessages, + uploadFilesAndImages, +} from "./utils"; import styles from "./ChatComponent.module.scss"; +import { BaseTextEditorHandles } from "@/shared/ui-kit/TextEditor/BaseTextEditor"; const BASE_CHAT_INPUT_HEIGHT = 48; @@ -157,6 +165,7 @@ export default function ChatComponent({ queryParams[QueryParamKey.Unchecked] === "true"; const { checkImageSize } = useImageSizeCheck(); useZoomDisabling(); + const textInputRef = useRef(null); const editorRef = useRef(null); const [inputContainerRef, { height: chatInputHeight }] = useMeasure(); @@ -260,6 +269,51 @@ export default function ChatComponent({ const prevFeedItemId = useRef(); const timeoutId = useRef | null>(); + const optimisticFeedItems = useSelector(selectOptimisticFeedItems); + + const optimisticDiscussionMessages = useSelector( + selectOptimisticDiscussionMessages, + ); + + const isOptimisticChat = optimisticFeedItems.has(discussionId); + + useEffect(() => { + if (optimisticDiscussionMessages.size > 0) { + const entries = Array.from(optimisticDiscussionMessages.entries()); + (async () => { + await Promise.all( + entries.map(async ([optimisticMessageDiscussionId, messages]) => { + if (!optimisticFeedItems.has(optimisticMessageDiscussionId)) { + const newMessagesWithFiles = await uploadFilesAndImages(messages); + await sendMessages({ + newMessagesWithFiles, + updateChatMessage: chatMessagesData.updateChatMessage, + chatChannel, + discussionId: optimisticMessageDiscussionId, + dispatch, + }); + + dispatch( + commonActions.clearOptimisticDiscussionMessages( + optimisticMessageDiscussionId, + ), + ); + + return messages; + } + + return messages; + }), + ); + })(); + } + }, [ + optimisticFeedItems, + optimisticDiscussionMessages, + chatChannel, + chatMessagesData.updateChatMessage, + ]); + useEffect(() => { return () => { prevFeedItemId.current = feedItemId; @@ -346,71 +400,13 @@ export default function ChatComponent({ useDebounce( async () => { - const newMessagesWithFiles = await Promise.all( - newMessages.map(async (payload) => { - const [uploadedFiles, uploadedImages] = await Promise.all([ - FileService.uploadFiles( - (payload.filesPreview ?? []).map((file) => - FileService.convertFileInfoToUploadFile(file), - ), - ), - FileService.uploadFiles( - (payload.imagesPreview ?? []).map((file) => - FileService.convertFileInfoToUploadFile(file), - ), - ), - ]); - - const updatedPayload = omit(payload, [ - "filesPreview", - "imagesPreview", - ]); - - return { - ...updatedPayload, - images: uploadedImages, - files: uploadedFiles, - }; - }), - ); - - newMessagesWithFiles.map(async (payload, index) => { - delay(async () => { - const pendingMessageId = payload.pendingMessageId as string; - - if (chatChannel) { - const response = await ChatService.sendChatMessage({ - id: pendingMessageId, - chatChannelId: chatChannel.id, - text: payload.text || "", - images: payload.images, - files: payload.files, - mentions: payload.tags?.map((tag) => tag.value), - parentId: payload.parentId, - hasUncheckedItems: checkUncheckedItemsInTextEditorValue( - parseStringToTextEditorValue(payload.text), - ), - linkPreviews: payload.linkPreviews, - }); - chatMessagesData.updateChatMessage(response); - - return; - } - - const response = await DiscussionMessageService.createMessage({ - ...payload, - id: pendingMessageId, - }); - - dispatch( - cacheActions.updateDiscussionMessageWithActualId({ - discussionId, - pendingMessageId, - actualId: response.id, - }), - ); - }, 2000 * (index || 1)); - return payload; + const newMessagesWithFiles = await uploadFilesAndImages(newMessages); + await sendMessages({ + newMessagesWithFiles, + updateChatMessage: chatMessagesData.updateChatMessage, + chatChannel, + discussionId, + dispatch, }); if (newMessages.length > 0) { @@ -575,13 +571,17 @@ export default function ChatComponent({ }); } - setMessages((prev) => { - if (isFilesMessageWithoutTextAndImages) { - return [...prev, ...filePreviewPayload]; - } + if (isOptimisticChat) { + dispatch(commonActions.setOptimisticDiscussionMessages(payload)); + } else { + setMessages((prev) => { + if (isFilesMessageWithoutTextAndImages) { + return [...prev, ...filePreviewPayload]; + } - return [...prev, ...filePreviewPayload, payload]; - }); + return [...prev, ...filePreviewPayload, payload]; + }); + } if (isChatChannel) { pendingMessages.forEach((pendingMessage) => { @@ -618,6 +618,7 @@ export default function ChatComponent({ discussionMessages, isChatChannel, linkPreviewData, + isOptimisticChat, ], ); @@ -719,6 +720,11 @@ export default function ChatComponent({ } }, [discussionMessageReply, currentFilesPreview]); + useLayoutEffect(() => { + textInputRef?.current?.clear?.(); + textInputRef?.current?.focus?.(); + },[discussionId]); + useEffect(() => { if (isFetchedDiscussionMessages) { onMessagesAmountChange?.(discussionMessages.length); @@ -853,6 +859,7 @@ export default function ChatComponent({ })} > void; inputContainerRef?: - | MutableRefObject - | RefCallback; + | MutableRefObject + | RefCallback; editorRef?: MutableRefObject | RefCallback; renderChatInputOuter?: () => ReactElement; isAuthorized?: boolean; } -export const ChatInput = (props: ChatInputProps): ReactElement | null => { +export const ChatInput = React.memo(forwardRef((props, ref): ReactElement | null => { const { inputContainerRef, editorRef, @@ -93,6 +95,7 @@ export const ChatInput = (props: ChatInputProps): ReactElement | null => { accept={FILES_ACCEPTED_EXTENSIONS} /> { ); -}; +})); diff --git a/src/pages/common/components/ChatComponent/utils/index.ts b/src/pages/common/components/ChatComponent/utils/index.ts index babe1dcca9..e2ec04042b 100644 --- a/src/pages/common/components/ChatComponent/utils/index.ts +++ b/src/pages/common/components/ChatComponent/utils/index.ts @@ -1 +1,3 @@ export * from "./getLastNonUserMessage"; +export * from "./uploadFilesAndImages"; +export * from "./sendMessages"; \ No newline at end of file diff --git a/src/pages/common/components/ChatComponent/utils/sendMessages.ts b/src/pages/common/components/ChatComponent/utils/sendMessages.ts new file mode 100644 index 0000000000..5becdbac2d --- /dev/null +++ b/src/pages/common/components/ChatComponent/utils/sendMessages.ts @@ -0,0 +1,49 @@ +import { ChatService, DiscussionMessageService } from "@/services"; +import { parseStringToTextEditorValue } from "@/shared/ui-kit"; +import { checkUncheckedItemsInTextEditorValue } from "@/shared/ui-kit/TextEditor/utils"; +import { cacheActions } from "@/store/states"; +import { delay } from "lodash"; + +export const sendMessages = async ({ + newMessagesWithFiles, + updateChatMessage, + chatChannel, + discussionId, + dispatch +}) => { + newMessagesWithFiles.map(async (payload, index) => { + delay(async () => { + const pendingMessageId = payload.pendingMessageId as string; + + if (chatChannel) { + const response = await ChatService.sendChatMessage({ + id: pendingMessageId, + chatChannelId: chatChannel.id, + text: payload.text || "", + images: payload.images, + files: payload.files, + mentions: payload.tags?.map((tag) => tag.value), + parentId: payload.parentId, + hasUncheckedItems: checkUncheckedItemsInTextEditorValue( + parseStringToTextEditorValue(payload.text), + ), + linkPreviews: payload.linkPreviews, + }); + updateChatMessage(response); + } else { + const response = await DiscussionMessageService.createMessage({ + ...payload, + id: pendingMessageId, + }); + + dispatch( + cacheActions.updateDiscussionMessageWithActualId({ + discussionId, + pendingMessageId, + actualId: response.id, + }), + ); + } + }, 2000 * (index || 1)); + }); +}; \ No newline at end of file diff --git a/src/pages/common/components/ChatComponent/utils/uploadFilesAndImages.ts b/src/pages/common/components/ChatComponent/utils/uploadFilesAndImages.ts new file mode 100644 index 0000000000..247dac5655 --- /dev/null +++ b/src/pages/common/components/ChatComponent/utils/uploadFilesAndImages.ts @@ -0,0 +1,29 @@ +import { FileService } from "@/services"; +import { omit } from "lodash"; + +export const uploadFilesAndImages = async (newMessages) => { + return await Promise.all( + newMessages.map(async (payload) => { + const [uploadedFiles, uploadedImages] = await Promise.all([ + FileService.uploadFiles( + (payload.filesPreview ?? []).map((file) => + FileService.convertFileInfoToUploadFile(file), + ), + ), + FileService.uploadFiles( + (payload.imagesPreview ?? []).map((file) => + FileService.convertFileInfoToUploadFile(file), + ), + ), + ]); + + const updatedPayload = omit(payload, ["filesPreview", "imagesPreview"]); + + return { + ...updatedPayload, + images: uploadedImages, + files: uploadedFiles, + }; + }), + ); +}; \ No newline at end of file diff --git a/src/pages/common/components/CommonMemberInfo/components/PopoverItem/PopoverItem.tsx b/src/pages/common/components/CommonMemberInfo/components/PopoverItem/PopoverItem.tsx index 04275c28bd..cc8450e069 100644 --- a/src/pages/common/components/CommonMemberInfo/components/PopoverItem/PopoverItem.tsx +++ b/src/pages/common/components/CommonMemberInfo/components/PopoverItem/PopoverItem.tsx @@ -1,5 +1,6 @@ import React, { FC, useCallback } from "react"; import classNames from "classnames"; +import { v4 as uuidv4 } from "uuid"; import { useCommonDataContext } from "@/pages/common/providers"; import { Circle } from "@/shared/models"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui-kit"; @@ -103,6 +104,8 @@ export const PopoverItem: FC = (props) => { onJoinCircle( { args: { + id: uuidv4(), + discussionId: uuidv4(), commonId, title: `Request to join ${circleName} by ${userName}`, description: `Join request: ${circleName}`, 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 e32b147eb9..e30f9ff74c 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 @@ -1,5 +1,6 @@ import React, { FC, useCallback, useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { NewDiscussionCreationFormValues, @@ -9,6 +10,7 @@ import { Circle, CirclesPermissions, Common, + CommonFeedType, CommonMember, Governance, } from "@/shared/models"; @@ -16,12 +18,14 @@ import { TextEditorValue, parseStringToTextEditorValue, } from "@/shared/ui-kit/TextEditor"; +import { generateFirstMessage, generateOptimisticFeedItem, getUserName } from "@/shared/utils"; import { selectDiscussionCreationData, selectIsDiscussionCreationLoading, } from "@/store/states"; import { commonActions } from "@/store/states"; import { DiscussionCreationCard, DiscussionCreationModal } from "./components"; +import { DiscussionMessageOwnerType } from "@/shared/constants"; interface NewDiscussionCreationProps { common: Common; @@ -116,9 +120,32 @@ 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}), + } + }), + ), + ); + dispatch( commonActions.createDiscussion.request({ payload: { + id: discussionId, title: values.title, message: JSON.stringify(values.content), ownerId: userId, @@ -129,6 +156,8 @@ const NewDiscussionCreation: FC = (props) => { }), ); } + + handleCancel(); }, [governanceCircles, userCircleIds, userId, common.id, edit], ); diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/components/DiscussionForm/DiscussionForm.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/components/DiscussionForm/DiscussionForm.tsx index 06ff7aa9cf..3f65312069 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/components/DiscussionForm/DiscussionForm.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewDiscussionCreation/components/DiscussionForm/DiscussionForm.tsx @@ -1,4 +1,4 @@ -import React, { FC } from "react"; +import React, { FC, useEffect, useRef } from "react"; import classNames from "classnames"; import { TextEditor, @@ -17,9 +17,18 @@ interface DiscussionFormProps { const DiscussionForm: FC = (props) => { const { className, disabled = false } = props; + const textEditorRef = useRef(null); + + useEffect(() => { + if (textEditorRef.current) { + textEditorRef.current.focus(); + } + }, []); + return (
= (props) => { return; } + const proposalId = uuidv4(); + 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}), + } + }), + ), + ); switch (values.proposalType.value) { case ProposalsTypes.FUNDS_ALLOCATION: { const fundingProposalPayload = getFundingProposalPayload( values, commonId, userId, + proposalId, + discussionId, ); if (!fundingProposalPayload) { @@ -103,12 +132,19 @@ const NewProposalCreation: FC = (props) => { case ProposalsTypes.SURVEY: { dispatch( commonActions.createSurveyProposal.request({ - payload: getSurveyProposalPayload(values, commonId), + payload: getSurveyProposalPayload( + values, + commonId, + proposalId, + discussionId, + ), }), ); break; } } + + handleCancel(); }, [governance.circles, userCircleIds, userId, commonId], ); diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/util.ts b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/util.ts index 75a7003982..b1a4ff5d4f 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/util.ts +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewProposalCreation/util.ts @@ -10,6 +10,8 @@ export const getFundingProposalPayload = ( values: NewProposalCreationFormValues, commonId: string, userId: string, + proposalId: string, + discussionId: string, ): CreateProposalWithFiles | null => { if (!values.recipientInfo) { return null; @@ -21,6 +23,8 @@ export const getFundingProposalPayload = ( : AllocateFundsTo.OtherMember; return { + id: proposalId, + discussionId, title: values.title, description: JSON.stringify(values.content), images: values.images, @@ -48,8 +52,12 @@ export const getFundingProposalPayload = ( export const getSurveyProposalPayload = ( values: NewProposalCreationFormValues, commonId: string, + proposalId: string, + discussionId: string, ): CreateProposalWithFiles => { return { + id: proposalId, + discussionId, title: values.title, description: JSON.stringify(values.content), images: values.images, diff --git a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx index 6117a8a25f..0ecf31b59d 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -5,13 +5,13 @@ import React, { useMemo, useState, } from "react"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { useUpdateEffect } from "react-use"; import { debounce } from "lodash"; import { selectUser } from "@/pages/Auth/store/selectors"; import { DiscussionService } from "@/services"; import { DeletePrompt, GlobalOverlay, ReportModal } from "@/shared/components"; -import { EntityTypes, InboxItemType } from "@/shared/constants"; +import { DiscussionMessageOwnerType, EntityTypes, InboxItemType } from "@/shared/constants"; import { useModal, useNotification } from "@/shared/hooks"; import { FeedItemFollowState, @@ -33,7 +33,7 @@ import { PredefinedTypes, } from "@/shared/models"; import { TextEditorValue } from "@/shared/ui-kit"; -import { StaticLinkType, getUserName, InternalLinkData } from "@/shared/utils"; +import { StaticLinkType, getUserName, InternalLinkData, generateFirstMessage } from "@/shared/utils"; import { useChatContext } from "../ChatComponent"; import { FeedCard } from "../FeedCard"; import { FeedCardShare } from "../FeedCard"; @@ -49,6 +49,7 @@ import { DiscussionFeedCardContent, } from "./components"; import { useMenuItems } from "./hooks"; +import { commonActions } from "@/store/states"; interface DiscussionFeedCardProps { item: CommonFeed; @@ -76,6 +77,7 @@ interface DiscussionFeedCardProps { onUserClick?: (userId: string) => void; onFeedItemClick: (feedItemId: string) => void; onInternalLinkClick: (data: InternalLinkData) => void; + isOptimisticallyCreated?: boolean; } function DiscussionFeedCard(props, ref) { @@ -86,6 +88,7 @@ function DiscussionFeedCard(props, ref) { nestedItemData, } = useChatContext(); const { notify } = useNotification(); + const dispatch = useDispatch(); const { item, governanceCircles, @@ -112,6 +115,7 @@ function DiscussionFeedCard(props, ref) { onUserClick, onFeedItemClick, onInternalLinkClick, + isOptimisticallyCreated, } = props; const { isShowing: isReportModalOpen, @@ -213,6 +217,12 @@ 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({ @@ -247,6 +257,7 @@ function DiscussionFeedCard(props, ref) { feedItemUserMetadata?.hasUnseenMention, nestedItemData, isPreviewMode, + isActive, ]); const onDiscussionDelete = useCallback(async () => { @@ -273,6 +284,13 @@ function DiscussionFeedCard(props, ref) { [preloadDiscussionMessagesData.preloadDiscussionMessages], ); + useEffect(() => { + if(item.data.lastMessage?.content && discussion?.id && isOptimisticallyCreated) { + // markFeedItemAsSeen({feedObjectId: item.id, commonId}) + dispatch(commonActions.clearCreatedOptimisticFeedItem(discussion?.id)); + } + },[item.id, item.data.lastMessage?.content, discussion?.id, isOptimisticallyCreated, commonId]) + useEffect(() => { fetchDiscussionCreator(item.userId); }, [item.userId]); @@ -347,12 +365,21 @@ function DiscussionFeedCard(props, ref) { }, [item.data.lastMessage?.content]); const lastMessage = useMemo(() => { + const userName = getUserName(discussionCreator); + + const optimisticMessage = { + userName, + ownerId: userId, + content: generateFirstMessage({userName, userId: userId ?? ""}), + ownerType: DiscussionMessageOwnerType.System, + } + return getLastMessage({ commonFeedType: item.data.type, - lastMessage: item.data.lastMessage, + lastMessage: isOptimisticallyCreated ? optimisticMessage : item.data.lastMessage, discussion, currentUserId: userId, - feedItemCreatorName: getUserName(discussionCreator), + feedItemCreatorName: userName, commonName, isProject, hasFiles: item.data.hasFiles, @@ -368,6 +395,7 @@ function DiscussionFeedCard(props, ref) { isProject, item.data.hasFiles, item.data.hasImages, + isOptimisticallyCreated, ]); return ( @@ -390,13 +418,13 @@ function DiscussionFeedCard(props, ref) { image={commonImage} imageAlt={`${commonName}'s image`} isProject={isProject} - isFollowing={feedItemFollow.isFollowing} + isFollowing={isOptimisticallyCreated || feedItemFollow.isFollowing} isLoading={isLoading} menuItems={menuItems} seenOnce={ feedItemUserMetadata?.seenOnce ?? !isFeedItemUserMetadataFetched } - seen={feedItemUserMetadata?.seen ?? !isFeedItemUserMetadataFetched} + seen={(isOptimisticallyCreated || feedItemUserMetadata?.seen) ?? !isFeedItemUserMetadataFetched} ownerId={item.userId} discussionPredefinedType={discussion?.predefinedType} notion={discussionNotion && commonNotion} diff --git a/src/pages/common/components/FeedCard/FeedCard.tsx b/src/pages/common/components/FeedCard/FeedCard.tsx index 8a786f1bdd..28860dc8a3 100644 --- a/src/pages/common/components/FeedCard/FeedCard.tsx +++ b/src/pages/common/components/FeedCard/FeedCard.tsx @@ -243,7 +243,7 @@ const FeedCard = (props, ref) => { onClick: handleClick, onExpand: handleExpand, title, - lastMessage: !isLoading ? lastMessage : undefined, + lastMessage, menuItems, commonName, commonId, diff --git a/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx b/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx index 1a170e718e..5be69171b8 100644 --- a/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx +++ b/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx @@ -19,7 +19,7 @@ interface FeedCardTagsProps { hasUnseenMention?: boolean; } -export const FeedCardTags: FC = (props) => { +export const MemoizedFeedCardTags: FC = (props) => { const { unreadMessages, type, @@ -35,9 +35,8 @@ export const FeedCardTags: FC = (props) => { const isOwner = ownerId === user?.uid; const isNewTagVisible = notEmpty(seenOnce) && notEmpty(isOwner) && !seenOnce && !isOwner; - const isUnseenTagVisible = - !isNewTagVisible && !unreadMessages && notEmpty(seen) && !seen; - + const isUnseenTagVisible = + !isNewTagVisible && !unreadMessages && notEmpty(seen) && !seen && !isOwner; return ( <> {type === CommonFeedType.Proposal && ( @@ -86,3 +85,5 @@ export const FeedCardTags: FC = (props) => { ); }; + +export const FeedCardTags = React.memo(MemoizedFeedCardTags); \ No newline at end of file diff --git a/src/pages/common/components/FeedItem/FeedItem.tsx b/src/pages/common/components/FeedItem/FeedItem.tsx index 56b8afca40..15a8d10276 100644 --- a/src/pages/common/components/FeedItem/FeedItem.tsx +++ b/src/pages/common/components/FeedItem/FeedItem.tsx @@ -20,6 +20,7 @@ import { import { checkIsItemVisibleForUser } from "@/shared/utils"; import { useFeedItemSubscription } from "../../hooks"; import { DiscussionFeedCard } from "../DiscussionFeedCard"; +import { OptimisticFeedCard } from "../OptimisticFeedCard"; import { ProposalFeedCard } from "../ProposalFeedCard"; import { ProjectFeedItem } from "./components"; import { useFeedItemContext } from "./context"; @@ -55,6 +56,7 @@ interface FeedItemProps { withoutMenu?: boolean; onFeedItemUpdate?: (item: CommonFeed, isRemoved: boolean) => void; getNonAllowedItems?: GetNonAllowedItemsOptions; + isOptimisticallyCreated?: boolean; } const FeedItem = forwardRef((props, ref) => { @@ -85,6 +87,7 @@ const FeedItem = forwardRef((props, ref) => { level, onFeedItemUpdate: outerOnFeedItemUpdate, getNonAllowedItems: outerGetNonAllowedItems, + isOptimisticallyCreated = false, } = props; const { onFeedItemUpdate, @@ -144,7 +147,6 @@ const FeedItem = forwardRef((props, ref) => { onFeedItemUnfollowed, ]); - const generalProps = useMemo( () => ({ ref, @@ -206,7 +208,6 @@ const FeedItem = forwardRef((props, ref) => { ], ); - if ( shouldCheckItemVisibility && !checkIsItemVisibleForUser({ @@ -220,8 +221,21 @@ const FeedItem = forwardRef((props, ref) => { return null; } + if ( + item.data.type === CommonFeedType.OptimisticDiscussion || + item.data.type === CommonFeedType.OptimisticProposal + ) { + return ( + + ); + } + if (item.data.type === CommonFeedType.Discussion) { - return ; + return ; } if (item.data.type === CommonFeedType.Proposal) { diff --git a/src/pages/common/components/FeedItems/FeedItems.tsx b/src/pages/common/components/FeedItems/FeedItems.tsx index cf27ab1b2f..5e71b1043f 100644 --- a/src/pages/common/components/FeedItems/FeedItems.tsx +++ b/src/pages/common/components/FeedItems/FeedItems.tsx @@ -70,6 +70,7 @@ const FeedItems: FC = (props) => { const isPinned = (common.pinnedFeedItems || []).some( (pinnedItem) => pinnedItem.feedObjectId === item.feedItem.id, ); + return ( > = ( )?.name; const payload = { + id: uuidv4(), commonId, description: message, images: [], diff --git a/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx b/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx new file mode 100644 index 0000000000..458e664088 --- /dev/null +++ b/src/pages/common/components/OptimisticFeedCard/OptimisticFeedCard.tsx @@ -0,0 +1,265 @@ +import React, { + forwardRef, + useCallback, + useEffect, + useMemo, +} from "react"; +import { useSelector } from "react-redux"; +import { useUpdateEffect } from "react-use"; +import { debounce } from "lodash"; +import { selectUser } from "@/pages/Auth/store/selectors"; +import { InboxItemType } from "@/shared/constants"; +import { + FeedItemFollowState, + useCommon, + useFeedItemUserMetadata, + usePreloadDiscussionMessagesById, +} from "@/shared/hooks/useCases"; +import { FeedLayoutItemChangeData } from "@/shared/interfaces"; +import { + Common, + CommonFeed, + CommonFeedType, + CommonMember, + CommonNotion, + DirectParent, + DiscussionWithOptimisticData, + Governance, +} from "@/shared/models"; +import { TextEditorValue } from "@/shared/ui-kit"; +import { InternalLinkData } from "@/shared/utils"; +import { useChatContext } from "../ChatComponent"; +import { FeedCard } from "../FeedCard"; +import { + FeedItemRef, + GetLastMessageOptions, + GetNonAllowedItemsOptions, +} from "../FeedItem"; + +interface OptimisticFeedCardProps { + item: CommonFeed; + governanceCircles?: Governance["circles"]; + isMobileVersion?: boolean; + commonId?: string; + commonName: string; + commonImage: string; + commonNotion?: CommonNotion; + pinnedFeedItems?: Common["pinnedFeedItems"]; + commonMember?: CommonMember | null; + isProject: boolean; + isPinned: boolean; + isPreviewMode: boolean; + isActive: boolean; + isExpanded: boolean; + getLastMessage: (options: GetLastMessageOptions) => TextEditorValue; + getNonAllowedItems?: GetNonAllowedItemsOptions; + onActiveItemDataChange?: (data: FeedLayoutItemChangeData) => void; + discussion?: DiscussionWithOptimisticData; + directParent?: DirectParent | null; + rootCommonId?: string; + feedItemFollow: FeedItemFollowState; + shouldPreLoadMessages: boolean; + withoutMenu?: boolean; + onUserClick?: (userId: string) => void; + onFeedItemClick: (feedItemId: string) => void; + onInternalLinkClick: (data: InternalLinkData) => void; + type: CommonFeedType; +} + +const OptimisticFeedCard = forwardRef< + FeedItemRef, + OptimisticFeedCardProps +>((props, ref) => { + const { + setChatItem, + feedItemIdForAutoChatOpen, + shouldAllowChatAutoOpen, + nestedItemData, + } = useChatContext(); + const { + item, + isMobileVersion = false, + commonId, + commonName, + commonImage, + commonNotion: outerCommonNotion, + isProject, + discussion, + isPreviewMode, + isActive, + isExpanded, + getLastMessage, + onActiveItemDataChange, + shouldPreLoadMessages, + onUserClick, + onFeedItemClick, + onInternalLinkClick, + } = props; + + const isHome = false; + const discussionNotion = undefined; + const { + data: feedItemUserMetadata, + fetched: isFeedItemUserMetadataFetched, + fetchFeedItemUserMetadata, + } = useFeedItemUserMetadata(); + const shouldLoadCommonData = + isHome || (discussionNotion && !outerCommonNotion); + const { data: common } = useCommon(shouldLoadCommonData ? commonId : ""); + const preloadDiscussionMessagesData = usePreloadDiscussionMessagesById({ + commonId, + discussionId: discussion?.id, + onUserClick, + onFeedItemClick, + onInternalLinkClick, + }); + const menuItems = []; + const user = useSelector(selectUser()); + const userId = user?.uid; + const cardTitle = discussion?.title; + const commonNotion = outerCommonNotion ?? common?.notion; + + const handleOpenChat = useCallback(() => { + if (discussion && !isPreviewMode) { + setChatItem({ + feedItemId: item.id, + discussion, + circleVisibility: item.circleVisibility, + lastSeenItem: feedItemUserMetadata?.lastSeen, + lastSeenAt: feedItemUserMetadata?.lastSeenAt, + count: feedItemUserMetadata?.count, + seenOnce: feedItemUserMetadata?.seenOnce, + seen: feedItemUserMetadata?.seen, + hasUnseenMention: feedItemUserMetadata?.hasUnseenMention, + nestedItemData: nestedItemData && { + ...nestedItemData, + feedItem: { + type: InboxItemType.FeedItemFollow, + itemId: item.id, + feedItem: item, + }, + }, + }); + } + }, [ + discussion, + item.id, + item.circleVisibility, + feedItemUserMetadata?.lastSeen, + feedItemUserMetadata?.lastSeenAt, + feedItemUserMetadata?.count, + feedItemUserMetadata?.seenOnce, + feedItemUserMetadata?.seen, + feedItemUserMetadata?.hasUnseenMention, + nestedItemData, + isPreviewMode, + ]); + + const preloadDiscussionMessages = useMemo( + () => + debounce( + (...args) => + preloadDiscussionMessagesData.preloadDiscussionMessages(...args), + 6000, + ), + [preloadDiscussionMessagesData.preloadDiscussionMessages], + ); + + useEffect(() => { + if (commonId) { + fetchFeedItemUserMetadata({ + userId: userId || "", + commonId, + feedObjectId: item.id, + }); + } + }, [userId, commonId, item.id]); + + useEffect(() => { + if ( + (!isActive || + shouldAllowChatAutoOpen === null || + shouldAllowChatAutoOpen) && + isFeedItemUserMetadataFetched && + item.id === feedItemIdForAutoChatOpen && + !isMobileVersion + ) { + handleOpenChat(); + } + }, [isFeedItemUserMetadataFetched, shouldAllowChatAutoOpen]); + + useEffect(() => { + if (isActive && shouldAllowChatAutoOpen !== null) { + handleOpenChat(); + } + }, [isActive, shouldAllowChatAutoOpen, handleOpenChat]); + + useEffect(() => { + if (isActive && cardTitle) { + onActiveItemDataChange?.({ + itemId: item.id, + title: cardTitle, + }); + } + }, [isActive, cardTitle]); + + useEffect(() => { + if ( + shouldPreLoadMessages && + !isActive && + commonId && + item.circleVisibility + ) { + preloadDiscussionMessages(item.circleVisibility); + } + }, [shouldPreLoadMessages, isActive]); + + useUpdateEffect(() => { + if ( + shouldPreLoadMessages && + !isActive && + commonId && + item.circleVisibility + ) { + preloadDiscussionMessages(item.circleVisibility, true); + } + }, [item.data.lastMessage?.content]); + + return ( + + ); +}); + +export default OptimisticFeedCard; diff --git a/src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/OptimisticFeedCardContent.tsx b/src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/OptimisticFeedCardContent.tsx new file mode 100644 index 0000000000..8bd1e086a7 --- /dev/null +++ b/src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/OptimisticFeedCardContent.tsx @@ -0,0 +1,114 @@ +import React from "react"; +import { ContextMenuItem } from "@/shared/interfaces"; +import { + Common, + CommonFeed, + CommonFeedType, + DirectParent, + DiscussionNotion, + Governance, + Link, + User, +} from "@/shared/models"; +import { getUserName } from "@/shared/utils"; +import { + FeedCardContent, + FeedCardHeader, + FeedCountdown, + getVisibilityString, +} from "../../../FeedCard"; + +interface OptimisticFeedCardContentProps { + item: CommonFeed; + governanceCircles?: Governance["circles"]; + isMobileVersion?: boolean; + commonId?: string; + directParent?: DirectParent | null; + onUserSelect?: (userId: string, commonId?: string) => void; + discussionCreator: User | null; + isHome: boolean; + menuItems: ContextMenuItem[]; + discussionMessage?: string; + discussionImages: Link[]; + common: Common | null; + discussionNotion?: DiscussionNotion; + handleOpenChat: () => void; + onHover: (isMouseEnter: boolean) => void; + isLoading: boolean; + type?: CommonFeedType; +} + +export function OptimisticFeedCardContent( + props: OptimisticFeedCardContentProps, +) { + const { + item, + governanceCircles, + isMobileVersion = false, + commonId, + directParent, + onUserSelect, + discussionCreator, + isHome, + menuItems, + common, + discussionNotion, + handleOpenChat, + onHover, + isLoading, + discussionMessage, + discussionImages, + type, + } = props; + + if (isLoading || !commonId) { + return null; + } + + const circleVisibility = governanceCircles + ? getVisibilityString(governanceCircles, item?.circleVisibility) + : undefined; + + return ( + <> + + Created:{" "} + + + } + type={ + type === CommonFeedType.OptimisticProposal ? "Proposal" : "Discussion" + } + circleVisibility={circleVisibility} + menuItems={menuItems} + isMobileVersion={isMobileVersion} + commonId={commonId} + userId={item.userId} + directParent={directParent} + onUserSelect={ + onUserSelect && (() => onUserSelect(item.userId, commonId)) + } + /> + { + onHover(true); + }} + onMouseLeave={() => { + onHover(false); + }} + /> + + ); +} diff --git a/src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/index.ts b/src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/index.ts new file mode 100644 index 0000000000..45174ea033 --- /dev/null +++ b/src/pages/common/components/OptimisticFeedCard/components/OptimisticFeedCardContent/index.ts @@ -0,0 +1 @@ +export * from "./OptimisticFeedCardContent"; \ No newline at end of file diff --git a/src/pages/common/components/OptimisticFeedCard/components/index.ts b/src/pages/common/components/OptimisticFeedCard/components/index.ts new file mode 100644 index 0000000000..b2b3ef2850 --- /dev/null +++ b/src/pages/common/components/OptimisticFeedCard/components/index.ts @@ -0,0 +1 @@ +export * from "./OptimisticFeedCardContent"; diff --git a/src/pages/common/components/OptimisticFeedCard/index.ts b/src/pages/common/components/OptimisticFeedCard/index.ts new file mode 100644 index 0000000000..8671496d94 --- /dev/null +++ b/src/pages/common/components/OptimisticFeedCard/index.ts @@ -0,0 +1 @@ +export { default as OptimisticFeedCard } from "./OptimisticFeedCard"; diff --git a/src/pages/common/components/index.ts b/src/pages/common/components/index.ts index e57e087668..5222245c9a 100644 --- a/src/pages/common/components/index.ts +++ b/src/pages/common/components/index.ts @@ -3,6 +3,7 @@ export * from "./CommonMobileModal"; export * from "./CommonTabPanels"; export * from "./CommonTopNavigation"; export * from "./DiscussionFeedCard"; +export * from "./OptimisticFeedCard"; export * from "./FeedCard"; export * from "./FeedItem"; export * from "./FeedItems"; diff --git a/src/pages/common/hooks/useJoinProjectAutomatically.ts b/src/pages/common/hooks/useJoinProjectAutomatically.ts index 31b51fbbd8..060ce3e4e2 100644 --- a/src/pages/common/hooks/useJoinProjectAutomatically.ts +++ b/src/pages/common/hooks/useJoinProjectAutomatically.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; +import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { ProposalService } from "@/services"; import { ProposalsTypes, SUPPORT_EMAIL } from "@/shared/constants"; @@ -135,9 +136,13 @@ export const useJoinProjectAutomatically = ( setIsJoinPending(true); + const proposalId = uuidv4(); + const discussionId = uuidv4(); try { await ProposalService.createAssignProposal({ args: { + id: proposalId, + discussionId, commonId: parentCommon.id, userId: user?.uid, circleId, diff --git a/src/pages/commonFeed/CommonFeed.tsx b/src/pages/commonFeed/CommonFeed.tsx index f578d5d96a..de5643757b 100644 --- a/src/pages/commonFeed/CommonFeed.tsx +++ b/src/pages/commonFeed/CommonFeed.tsx @@ -51,8 +51,10 @@ import { cacheActions, commonActions, selectCommonAction, + selectCreatedOptimisticFeedItems, selectFeedSearchValue, selectIsSearchingFeedItems, + selectOptimisticFeedItems, selectRecentStreamId, selectSharedFeedItem, } from "@/store/states"; @@ -114,6 +116,8 @@ const CommonFeedComponent: FC = (props) => { sharedFeedItemIdQueryParam) || null; const commonAction = useSelector(selectCommonAction); + const createdOptimisticFeedItems = useSelector(selectCreatedOptimisticFeedItems); + const optimisticFeedItems = useSelector(selectOptimisticFeedItems); const { data: commonData, stateRef, @@ -204,7 +208,7 @@ const CommonFeedComponent: FC = (props) => { ); const sharedFeedItem = useSelector(selectSharedFeedItem); - const topFeedItems = useMemo(() => { + const topFeedItemsWithoutOptimistic = useMemo(() => { const items: FeedLayoutItem[] = []; const filteredPinnedItems = commonPinnedFeedItems?.filter( @@ -220,6 +224,18 @@ const CommonFeedComponent: FC = (props) => { return items; }, [sharedFeedItem, sharedFeedItemId, commonPinnedFeedItems]); + + const topFeedItems = useMemo(() => { + const items: FeedLayoutItem[] = [...topFeedItemsWithoutOptimistic]; + + if (optimisticFeedItems.size > 0) { + const optimisticItems = Array.from(optimisticFeedItems.values()); + items.push(...optimisticItems); + } + + return items; + }, [topFeedItemsWithoutOptimistic, optimisticFeedItems]); + const firstItem = commonFeedItems?.[0]; const isDataFetched = isCommonDataFetched; const hasPublicItems = commonData?.common.hasPublicItems ?? false; @@ -452,10 +468,19 @@ const CommonFeedComponent: FC = (props) => { ) { feedLayoutRef?.setActiveItem({ feedItemId: firstItem.feedItem.id, + discussion: createdOptimisticFeedItems.get(recentStreamId)?.feedItem.optimisticData }); dispatch(commonActions.setRecentStreamId("")); + } else if ( + checkIsFeedItemFollowLayoutItem(firstItem) && + optimisticFeedItems.has(recentStreamId) + ) { + feedLayoutRef?.setActiveItem({ + feedItemId: optimisticFeedItems.get(recentStreamId)!.feedItem.id, + discussion: optimisticFeedItems.get(recentStreamId)?.feedItem.optimisticData, + }); } - }, [feedLayoutRef, recentStreamId, firstItem]); + }, [feedLayoutRef, recentStreamId, firstItem, optimisticFeedItems]); useEffect(() => { const handler: CommonEventToListener[CommonEvent.CommonDeleted] = ( diff --git a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx index 3eba160986..64ba269785 100644 --- a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx +++ b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx @@ -75,6 +75,7 @@ import { getParamsFromOneOfRoutes, getUserName, } from "@/shared/utils"; +import { selectCreatedOptimisticFeedItems, selectRecentStreamId } from "@/store/states"; import { MIN_CONTENT_WIDTH } from "../../constants"; import { DesktopChat, @@ -198,6 +199,8 @@ const FeedLayout: ForwardRefRenderFunction = ( const queryParams = useQueryParams(); const isTabletView = useIsTabletView(); const user = useSelector(selectUser()); + const createdOptimisticFeedItems = useSelector(selectCreatedOptimisticFeedItems); + const recentStreamId = useSelector(selectRecentStreamId); const userId = user?.uid; const [chatItem, setChatItem] = useState(); const [isShowFeedItemDetailsModal, setIsShowFeedItemDetailsModal] = @@ -284,6 +287,7 @@ const FeedLayout: ForwardRefRenderFunction = ( return items; }, [topFeedItems, feedItems]); + const dmChatChannelItemForProfile = useMemo( () => getDMChatChannelItemByUserIds( @@ -398,11 +402,6 @@ const FeedLayout: ForwardRefRenderFunction = ( const setActiveChatItem = useCallback((nextChatItem: ChatItem | null) => { setShouldAllowChatAutoOpen(false); - setExpandedFeedItemId((currentExpandedFeedItemId) => - currentExpandedFeedItemId === nextChatItem?.feedItemId - ? currentExpandedFeedItemId - : null, - ); setChatItem(nextChatItem); }, []); @@ -420,7 +419,6 @@ const FeedLayout: ForwardRefRenderFunction = ( const setActiveItem = useCallback((item: ChatItem) => { setShouldAllowChatAutoOpen(false); setChatItem(item); - setExpandedFeedItemId(item.feedItemId); }, []); const handleMessagesAmountChange = useCallback( @@ -716,7 +714,9 @@ const FeedLayout: ForwardRefRenderFunction = ( return; } - setActiveChatItem(null); + if(!recentStreamId) { + setActiveChatItem(null); + } if (!isTabletView) { setShouldAllowChatAutoOpen(true); @@ -863,6 +863,7 @@ const FeedLayout: ForwardRefRenderFunction = ( = (props) => { const { getProjectCreationPagePath } = useRoutesContext(); const handleNewSpace = () => history.push(getProjectCreationPagePath(commonId)); + const dispatch = useDispatch(); + + const onNewDiscussion = () => { + dispatch(commonActions.setCommonAction(CommonAction.NewDiscussion)); + animateScroll.scrollToTop({ containerId: document.body, smooth: true }); + }; const items = useMenuItems({ commonMember, governance, @@ -39,6 +49,14 @@ const NewStreamButton: FC = (props) => { return null; } + if(items.length === 2) { + return ( + + + + ) + } + const triggerEl = ( diff --git a/src/shared/components/Form/Formik/TextField/TextField.tsx b/src/shared/components/Form/Formik/TextField/TextField.tsx index c176f14fee..4763f9e6a3 100644 --- a/src/shared/components/Form/Formik/TextField/TextField.tsx +++ b/src/shared/components/Form/Formik/TextField/TextField.tsx @@ -1,14 +1,14 @@ -import React, { FC } from "react"; +import React, { forwardRef } from "react"; import { useField } from "formik"; import { useZoomDisabling } from "@/shared/hooks"; -import { Input, InputProps } from "../../Input"; +import { Input, InputProps, InputRef } from "../../Input"; export type TextFieldProps = InputProps & { isRequired?: boolean; value?: string; }; -const TextField: FC = (props) => { +const TextField = forwardRef((props, ref) => { const { isRequired, ...restProps } = props; const [field, { touched, error }] = useField(restProps); const hintToShow = restProps.hint || (isRequired ? "Required" : ""); @@ -16,12 +16,13 @@ const TextField: FC = (props) => { return ( ); -}; +}); export default TextField; diff --git a/src/shared/components/Form/Input/Input.tsx b/src/shared/components/Form/Input/Input.tsx index 9b1651be04..4c299b78ba 100644 --- a/src/shared/components/Form/Input/Input.tsx +++ b/src/shared/components/Form/Input/Input.tsx @@ -79,6 +79,8 @@ const Input: ForwardRefRenderFunction = ( ...restProps } = props; const innerInputRef = useRef(null); + const innerRef = useRef(null); + const [inputLengthRef, setInputLengthRef] = useState( null, ); @@ -133,7 +135,8 @@ const Input: ForwardRefRenderFunction = ( inputRef, () => ({ focus: () => { - innerInputRef.current?.focus(); + innerInputRef?.current?.focus(); + innerRef?.current?.focus(); }, }), [], @@ -189,10 +192,11 @@ const Input: ForwardRefRenderFunction = ( )} {restProps.isTextarea && (