diff --git a/public/index.html b/public/index.html index a11216879f..df420d1a82 100644 --- a/public/index.html +++ b/public/index.html @@ -46,9 +46,10 @@ } .loader-wrapper { - position: relative; + position: absolute; left: 50%; - transform: translateX(-50%); + top: 50%; + transform: translate(-50%, -50%); height: 75px; width: 75px; background-image: url(/icons/loader-pink.svg); 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/App/router/Router.tsx b/src/pages/App/router/Router.tsx index 26a09228a8..4a6dfb8f42 100644 --- a/src/pages/App/router/Router.tsx +++ b/src/pages/App/router/Router.tsx @@ -6,7 +6,7 @@ import { ROUTES } from "./configuration"; const Router: FC = () => ( {ROUTES.map((layoutConfiguration, index) => ( - + ))} diff --git a/src/pages/App/router/components/Layout/Layout.tsx b/src/pages/App/router/components/Layout/Layout.tsx index a59cc1c887..eddff8016a 100644 --- a/src/pages/App/router/components/Layout/Layout.tsx +++ b/src/pages/App/router/components/Layout/Layout.tsx @@ -1,7 +1,8 @@ -import React, { FC } from "react"; +import React, { FC, Suspense } from "react"; import { Route, Switch } from "react-router-dom"; import { LayoutConfigurationWithRouteProps } from "../../types"; import { LayoutRoute } from "../LayoutRoute"; +import { SuspenseLoader } from "@/shared/ui-kit"; const Layout: FC = (props) => { const { component: LayoutComponent, routes, ...restProps } = props; @@ -9,11 +10,11 @@ const Layout: FC = (props) => { return ( + - {routes.map((route) => ( - - ))} + {routes.map((route) => )} + ); diff --git a/src/pages/App/router/components/LayoutRoute/LayoutRoute.tsx b/src/pages/App/router/components/LayoutRoute/LayoutRoute.tsx index 3edb7b6c83..9d1238817b 100644 --- a/src/pages/App/router/components/LayoutRoute/LayoutRoute.tsx +++ b/src/pages/App/router/components/LayoutRoute/LayoutRoute.tsx @@ -1,4 +1,4 @@ -import React, { FC, useMemo } from "react"; +import React, { FC, Suspense, useMemo } from "react"; import { useSelector } from "react-redux"; import { Route, RouteProps } from "react-router-dom"; import { Route as RouteConfiguration } from "@/pages/App/router/types"; @@ -9,6 +9,7 @@ import { import { matchRoute } from "@/shared/utils"; import { LayoutRouteContext, LayoutRouteContextValue } from "./context"; import { renderRouteContent } from "./helpers"; +import { SuspenseLoader } from "@/shared/ui-kit"; interface PrivateRouteProps extends RouteProps { routeConfigurations: RouteConfiguration[]; @@ -36,19 +37,21 @@ const LayoutRoute: FC = (props) => { return ( - - renderRouteContent({ - ...routeProps, - component, - children, - configuration: routeConfiguration, - userRoles: userRoles || [], - authenticated, - }) - } - /> + + + renderRouteContent({ + ...routeProps, + component, + children, + configuration: routeConfiguration, + userRoles: userRoles || [], + authenticated, + }) + } + /> + ); }; diff --git a/src/pages/App/router/configuration/commonSidenavLayout.tsx b/src/pages/App/router/configuration/commonSidenavLayout.tsx index 4535543fda..acc9e932cb 100644 --- a/src/pages/App/router/configuration/commonSidenavLayout.tsx +++ b/src/pages/App/router/configuration/commonSidenavLayout.tsx @@ -1,76 +1,75 @@ -import { BillingPage_v04 } from "@/pages/billing"; -import { ALL_COMMON_PAGE_TABS, CommonPage_v04 } from "@/pages/common"; -import { - CommonCreationPage, - ProjectCreationPage_v04, -} from "@/pages/commonCreation"; -import { CommonEditingPage_v04 } from "@/pages/commonEditing"; -import { CommonFeedPage_v04 } from "@/pages/commonFeed"; -import { InboxPage_v04 } from "@/pages/inbox"; -import { ProfilePage_v04 } from "@/pages/profile"; -import { SettingsPage_v04 } from "@/pages/settings"; +import React, { lazy } from "react"; import { ROUTE_PATHS } from "@/shared/constants"; import { CommonSidenavLayout } from "@/shared/layouts"; import { LayoutConfiguration, RouteType } from "../types"; +import { ALL_COMMON_PAGE_TABS } from "@/pages/common"; + +const InboxPage_v04 = lazy(() => import("@/pages/inbox").then(module => ({ default: module.InboxPage_v04 }))); +const CommonCreationPage = lazy(() => import("@/pages/commonCreation").then(module => ({ default: module.CommonCreationPage }))); +const CommonFeedPage_v04 = lazy(() => import("@/pages/commonFeed").then(module => ({ default: module.CommonFeedPage_v04 }))); +const ProjectCreationPage_v04 = lazy(() => import("@/pages/commonCreation").then(module => ({ default: module.ProjectCreationPage_v04 }))); +const CommonEditingPage_v04 = lazy(() => import("@/pages/commonEditing").then(module => ({ default: module.CommonEditingPage_v04 }))); +const ProfilePage_v04 = lazy(() => import("@/pages/profile").then(module => ({ default: module.ProfilePage_v04 }))); +const BillingPage_v04 = lazy(() => import("@/pages/billing").then(module => ({ default: module.BillingPage_v04 }))); +const SettingsPage_v04 = lazy(() => import("@/pages/settings").then(module => ({ default: module.SettingsPage_v04 }))); +const CommonPage_v04 = lazy(() => import("@/pages/common").then(module => ({ default: module.CommonPage_v04 }))); export interface CommonSidenavLayoutRouteOptions { sidenav?: boolean; } -const getCommonPageConfiguration = - (): LayoutConfiguration["routes"] => - ALL_COMMON_PAGE_TABS.map((tab) => ({ - path: `${ROUTE_PATHS.V04_COMMON}/${tab}` as ROUTE_PATHS, - exact: true, - component: CommonPage_v04, - })); +const getCommonPageConfiguration = (): LayoutConfiguration["routes"] => + ALL_COMMON_PAGE_TABS.map((tab) => ({ + path: `${ROUTE_PATHS.V04_COMMON}/${tab}` as ROUTE_PATHS, + exact: true, + component: CommonPage_v04, + })); -export const COMMON_SIDENAV_LAYOUT_CONFIGURATION: LayoutConfiguration = - { - component: CommonSidenavLayout, - routes: [ - { - path: ROUTE_PATHS.V04_INBOX, - exact: true, - component: InboxPage_v04, - type: RouteType.Private, - unauthenticatedRedirectPath: ROUTE_PATHS.HOME, - }, - { - path: ROUTE_PATHS.V04_COMMON_CREATION, - exact: true, - component: CommonCreationPage, - }, - { - path: ROUTE_PATHS.V04_COMMON, - exact: true, - component: CommonFeedPage_v04, - }, - ...getCommonPageConfiguration(), - { - path: ROUTE_PATHS.V04_PROJECT_CREATION, - exact: true, - component: ProjectCreationPage_v04, - }, - { - path: ROUTE_PATHS.V04_COMMON_EDITING, - exact: true, - component: CommonEditingPage_v04, - }, - { - path: ROUTE_PATHS.V04_PROFILE, - exact: true, - component: ProfilePage_v04, - }, - { - path: ROUTE_PATHS.V04_BILLING, - exact: true, - component: BillingPage_v04, - }, - { - path: ROUTE_PATHS.V04_SETTINGS, - exact: true, - component: SettingsPage_v04, - }, - ], - }; +export const COMMON_SIDENAV_LAYOUT_CONFIGURATION: LayoutConfiguration = { + component: CommonSidenavLayout, + routes: [ + { + path: ROUTE_PATHS.V04_INBOX, + exact: true, + component: InboxPage_v04, + type: RouteType.Private, + unauthenticatedRedirectPath: ROUTE_PATHS.HOME, + }, + { + path: ROUTE_PATHS.V04_COMMON_CREATION, + exact: true, + component: CommonCreationPage, + }, + { + path: ROUTE_PATHS.V04_COMMON, + exact: true, + component: CommonFeedPage_v04, + }, + ...getCommonPageConfiguration(), + { + path: ROUTE_PATHS.V04_PROJECT_CREATION, + exact: true, + component: ProjectCreationPage_v04, + }, + { + path: ROUTE_PATHS.V04_COMMON_EDITING, + exact: true, + component: CommonEditingPage_v04, + }, + { + path: ROUTE_PATHS.V04_PROFILE, + exact: true, + component: ProfilePage_v04, + }, + { + path: ROUTE_PATHS.V04_BILLING, + exact: true, + component: BillingPage_v04, + }, + { + path: ROUTE_PATHS.V04_SETTINGS, + exact: true, + component: SettingsPage_v04, + }, + ], +}; diff --git a/src/pages/App/router/configuration/emptyLayout.tsx b/src/pages/App/router/configuration/emptyLayout.tsx index a794de5928..b2695d24f1 100644 --- a/src/pages/App/router/configuration/emptyLayout.tsx +++ b/src/pages/App/router/configuration/emptyLayout.tsx @@ -1,9 +1,12 @@ -import { PrivacyPolicy } from "@/pages/PrivacyPolicy"; -import { EmptyPage } from "@/pages/empty"; +import React, { lazy } from "react"; import { ROUTE_PATHS } from "@/shared/constants"; import { EmptyLayout } from "@/shared/layouts"; import { LayoutConfiguration } from "../types"; +// Wrapping non-default exports for lazy loading +const EmptyPage = lazy(() => import("@/pages/empty").then(module => ({ default: module.EmptyPage }))); +const PrivacyPolicy = lazy(() => import("@/pages/PrivacyPolicy").then(module => ({ default: module.PrivacyPolicy }))); + export const EMPTY_LAYOUT_CONFIGURATION: LayoutConfiguration = { component: EmptyLayout, routes: [ diff --git a/src/pages/App/router/configuration/multipleSpacesLayout.tsx b/src/pages/App/router/configuration/multipleSpacesLayout.tsx index 7c7cd751d4..9cec83fc37 100644 --- a/src/pages/App/router/configuration/multipleSpacesLayout.tsx +++ b/src/pages/App/router/configuration/multipleSpacesLayout.tsx @@ -1,17 +1,18 @@ -import { BillingPage } from "@/pages/billing"; -import { ALL_COMMON_PAGE_TABS, CommonPage } from "@/pages/common"; -import { - CommonCreationPage, - ProjectCreationPage, -} from "@/pages/commonCreation"; -import { CommonEditingPage } from "@/pages/commonEditing"; -import { CommonFeedPage } from "@/pages/commonFeed"; -import { InboxPage } from "@/pages/inbox"; -import { ProfilePage } from "@/pages/profile"; -import { SettingsPage } from "@/pages/settings"; +import React, { lazy } from "react"; import { ROUTE_PATHS } from "@/shared/constants"; import { MultipleSpacesLayout } from "@/shared/layouts"; import { LayoutConfiguration, RouteType } from "../types"; +import { ALL_COMMON_PAGE_TABS } from "@/pages/common"; + +const InboxPage = lazy(() => import("@/pages/inbox").then(module => ({ default: module.InboxPage }))); +const CommonCreationPage = lazy(() => import("@/pages/commonCreation").then(module => ({ default: module.CommonCreationPage }))); +const CommonFeedPage = lazy(() => import("@/pages/commonFeed").then(module => ({ default: module.CommonFeedPage }))); +const ProjectCreationPage = lazy(() => import("@/pages/commonCreation").then(module => ({ default: module.ProjectCreationPage }))); +const CommonEditingPage = lazy(() => import("@/pages/commonEditing").then(module => ({ default: module.CommonEditingPage }))); +const ProfilePage = lazy(() => import("@/pages/profile").then(module => ({ default: module.ProfilePage }))); +const BillingPage = lazy(() => import("@/pages/billing").then(module => ({ default: module.BillingPage }))); +const SettingsPage = lazy(() => import("@/pages/settings").then(module => ({ default: module.SettingsPage }))); +const CommonPage = lazy(() => import("@/pages/common").then(module => ({ default: module.CommonPage }))); export interface MultipleSpacesLayoutRouteOptions { withSidenav?: boolean; @@ -20,81 +21,79 @@ export interface MultipleSpacesLayoutRouteOptions { breadcrumbsItemsWithMenus?: boolean; } -const getCommonPageConfiguration = - (): LayoutConfiguration["routes"] => - ALL_COMMON_PAGE_TABS.map((tab) => ({ - path: `${ROUTE_PATHS.COMMON}/${tab}` as ROUTE_PATHS, +const getCommonPageConfiguration = (): LayoutConfiguration["routes"] => + ALL_COMMON_PAGE_TABS.map((tab) => ({ + path: `${ROUTE_PATHS.COMMON}/${tab}` as ROUTE_PATHS, + exact: true, + component: CommonPage, + routeOptions: { + withGoBack: true, + }, + })); + +export const MULTIPLE_SPACES_LAYOUT_CONFIGURATION: LayoutConfiguration = { + component: MultipleSpacesLayout, + routes: [ + { + path: ROUTE_PATHS.INBOX, exact: true, - component: CommonPage, + component: InboxPage, + type: RouteType.Private, + unauthenticatedRedirectPath: ROUTE_PATHS.HOME, routeOptions: { - withGoBack: true, - }, - })); - -export const MULTIPLE_SPACES_LAYOUT_CONFIGURATION: LayoutConfiguration = - { - component: MultipleSpacesLayout, - routes: [ - { - path: ROUTE_PATHS.INBOX, - exact: true, - component: InboxPage, - type: RouteType.Private, - unauthenticatedRedirectPath: ROUTE_PATHS.HOME, - routeOptions: { - breadcrumbsItemsWithMenus: false, - }, - }, - { - path: ROUTE_PATHS.COMMON_CREATION, - exact: true, - component: CommonCreationPage, + breadcrumbsItemsWithMenus: false, }, - { - path: ROUTE_PATHS.COMMON, - exact: true, - component: CommonFeedPage, - }, - ...getCommonPageConfiguration(), - { - path: ROUTE_PATHS.PROJECT_CREATION, - exact: true, - component: ProjectCreationPage, - }, - { - path: ROUTE_PATHS.COMMON_EDITING, - exact: true, - component: CommonEditingPage, - }, - { - path: ROUTE_PATHS.PROFILE, - exact: true, - component: ProfilePage, - routeOptions: { - withSidenav: false, - withBreadcrumbs: false, - withGoBack: true, - }, + }, + { + path: ROUTE_PATHS.COMMON_CREATION, + exact: true, + component: CommonCreationPage, + }, + { + path: ROUTE_PATHS.COMMON, + exact: true, + component: CommonFeedPage, + }, + ...getCommonPageConfiguration(), + { + path: ROUTE_PATHS.PROJECT_CREATION, + exact: true, + component: ProjectCreationPage, + }, + { + path: ROUTE_PATHS.COMMON_EDITING, + exact: true, + component: CommonEditingPage, + }, + { + path: ROUTE_PATHS.PROFILE, + exact: true, + component: ProfilePage, + routeOptions: { + withSidenav: false, + withBreadcrumbs: false, + withGoBack: true, }, - { - path: ROUTE_PATHS.BILLING, - exact: true, - component: BillingPage, - routeOptions: { - withSidenav: false, - withBreadcrumbs: false, - withGoBack: true, - }, + }, + { + path: ROUTE_PATHS.BILLING, + exact: true, + component: BillingPage, + routeOptions: { + withSidenav: false, + withBreadcrumbs: false, + withGoBack: true, }, - { - path: ROUTE_PATHS.SETTINGS, - exact: true, - component: SettingsPage, - routeOptions: { - withSidenav: false, - withBreadcrumbs: false, - withGoBack: true, - }, + }, + { + path: ROUTE_PATHS.SETTINGS, + exact: true, + component: SettingsPage, + routeOptions: { + withSidenav: false, + withBreadcrumbs: false, + withGoBack: true, }, - ], - }; + }, + ], +}; diff --git a/src/pages/App/router/configuration/oldLayout.tsx b/src/pages/App/router/configuration/oldLayout.tsx index bfb3bbaed6..6e4681d3e1 100644 --- a/src/pages/App/router/configuration/oldLayout.tsx +++ b/src/pages/App/router/configuration/oldLayout.tsx @@ -1,20 +1,21 @@ -import { SubmitInvoicesContainer } from "@/pages/Invoices"; -import { ContactUsContainer, LandingContainer } from "@/pages/Landing"; -import { MyAccountContainer } from "@/pages/MyAccount"; -import { - CommonDetailContainer, - DiscussionContainer, - DiscussionMessageContainer, - ProposalCommentContainer, - ProposalContainer, - SupportersContainer, -} from "@/pages/OldCommon"; -import { MyCommonsContainer } from "@/pages/OldCommon/containers/MyCommonsContainer"; -import { TrusteeContainer } from "@/pages/Trustee"; +import React, { lazy } from "react"; import { ROUTE_PATHS, ScreenSize } from "@/shared/constants"; import { OldLayout } from "@/shared/layouts"; import { LayoutConfiguration, RouteType } from "../types"; +const LandingContainer = lazy(() => import("@/pages/Landing").then(module => ({ default: module.LandingContainer }))); +const ContactUsContainer = lazy(() => import("@/pages/Landing").then(module => ({ default: module.ContactUsContainer }))); +const CommonDetailContainer = lazy(() => import("@/pages/OldCommon").then(module => ({ default: module.CommonDetailContainer }))); +const SupportersContainer = lazy(() => import("@/pages/OldCommon").then(module => ({ default: module.SupportersContainer }))); +const ProposalContainer = lazy(() => import("@/pages/OldCommon").then(module => ({ default: module.ProposalContainer }))); +const ProposalCommentContainer = lazy(() => import("@/pages/OldCommon").then(module => ({ default: module.ProposalCommentContainer }))); +const DiscussionContainer = lazy(() => import("@/pages/OldCommon").then(module => ({ default: module.DiscussionContainer }))); +const DiscussionMessageContainer = lazy(() => import("@/pages/OldCommon").then(module => ({ default: module.DiscussionMessageContainer }))); +const MyAccountContainer = lazy(() => import("@/pages/MyAccount").then(module => ({ default: module.MyAccountContainer }))); +const MyCommonsContainer = lazy(() => import("@/pages/OldCommon/containers/MyCommonsContainer").then(module => ({ default: module.MyCommonsContainer }))); +const SubmitInvoicesContainer = lazy(() => import("@/pages/Invoices").then(module => ({ default: module.SubmitInvoicesContainer }))); +const TrusteeContainer = lazy(() => import("@/pages/Trustee").then(module => ({ default: module.TrusteeContainer }))); + export interface OldLayoutRouteOptions { footer?: | boolean @@ -23,90 +24,89 @@ export interface OldLayoutRouteOptions { }; } -export const OLD_LAYOUT_CONFIGURATION: LayoutConfiguration = - { - component: OldLayout, - routes: [ - { - path: ROUTE_PATHS.HOME, - exact: true, - component: LandingContainer, - }, - { - path: ROUTE_PATHS.CONTACT_US, - exact: true, - component: ContactUsContainer, - }, - { - path: ROUTE_PATHS.V02_COMMON, - exact: true, - component: CommonDetailContainer, - routeOptions: { - footer: { - screenSizeWhenDisplay: ScreenSize.Desktop, - }, - }, - }, - { - path: ROUTE_PATHS.COMMON_SUPPORT, - exact: true, - component: SupportersContainer, - routeOptions: { - footer: false, +export const OLD_LAYOUT_CONFIGURATION: LayoutConfiguration = { + component: OldLayout, + routes: [ + { + path: ROUTE_PATHS.HOME, + exact: true, + component: LandingContainer, + }, + { + path: ROUTE_PATHS.CONTACT_US, + exact: true, + component: ContactUsContainer, + }, + { + path: ROUTE_PATHS.V02_COMMON, + exact: true, + component: CommonDetailContainer, + routeOptions: { + footer: { + screenSizeWhenDisplay: ScreenSize.Desktop, }, }, - { - path: ROUTE_PATHS.PROPOSAL_DETAIL, - component: ProposalContainer, - type: RouteType.Private, + }, + { + path: ROUTE_PATHS.COMMON_SUPPORT, + exact: true, + component: SupportersContainer, + routeOptions: { + footer: false, }, - { - path: ROUTE_PATHS.PROPOSAL_COMMENT, - component: ProposalCommentContainer, - type: RouteType.Private, - routeOptions: { - footer: { - screenSizeWhenDisplay: ScreenSize.Desktop, - }, + }, + { + path: ROUTE_PATHS.PROPOSAL_DETAIL, + component: ProposalContainer, + type: RouteType.Private, + }, + { + path: ROUTE_PATHS.PROPOSAL_COMMENT, + component: ProposalCommentContainer, + type: RouteType.Private, + routeOptions: { + footer: { + screenSizeWhenDisplay: ScreenSize.Desktop, }, }, - { - path: ROUTE_PATHS.DISCUSSION_DETAIL, - component: DiscussionContainer, - type: RouteType.Private, - routeOptions: { - footer: { - screenSizeWhenDisplay: ScreenSize.Desktop, - }, + }, + { + path: ROUTE_PATHS.DISCUSSION_DETAIL, + component: DiscussionContainer, + type: RouteType.Private, + routeOptions: { + footer: { + screenSizeWhenDisplay: ScreenSize.Desktop, }, }, - { - path: ROUTE_PATHS.DISCUSSION_MESSAGE, - component: DiscussionMessageContainer, - type: RouteType.Private, - routeOptions: { - footer: { - screenSizeWhenDisplay: ScreenSize.Desktop, - }, + }, + { + path: ROUTE_PATHS.DISCUSSION_MESSAGE, + component: DiscussionMessageContainer, + type: RouteType.Private, + routeOptions: { + footer: { + screenSizeWhenDisplay: ScreenSize.Desktop, }, }, - { - path: ROUTE_PATHS.MY_ACCOUNT, - component: MyAccountContainer, - type: RouteType.Private, - }, - { - path: ROUTE_PATHS.MY_COMMONS, - component: MyCommonsContainer, - type: RouteType.Private, - }, - { - path: ROUTE_PATHS.SUBMIT_INVOICES, - component: SubmitInvoicesContainer, - }, - { - path: ROUTE_PATHS.TRUSTEE, - component: TrusteeContainer, - }, - ], - }; + }, + { + path: ROUTE_PATHS.MY_ACCOUNT, + component: MyAccountContainer, + type: RouteType.Private, + }, + { + path: ROUTE_PATHS.MY_COMMONS, + component: MyCommonsContainer, + type: RouteType.Private, + }, + { + path: ROUTE_PATHS.SUBMIT_INVOICES, + component: SubmitInvoicesContainer, + }, + { + path: ROUTE_PATHS.TRUSTEE, + component: TrusteeContainer, + }, + ], +}; diff --git a/src/pages/App/router/configuration/sidenavLayout.tsx b/src/pages/App/router/configuration/sidenavLayout.tsx index 8dd4a8b732..f351e56336 100644 --- a/src/pages/App/router/configuration/sidenavLayout.tsx +++ b/src/pages/App/router/configuration/sidenavLayout.tsx @@ -1,20 +1,21 @@ -import { CommonPage_v03 } from "@/pages/common-v03"; +import React, { lazy } from "react"; import { ROUTE_PATHS } from "@/shared/constants"; import { SidenavLayout } from "@/shared/layouts"; import { LayoutConfiguration } from "../types"; +const CommonPage_v03 = lazy(() => import("@/pages/common-v03").then(module => ({ default: module.CommonPage_v03 }))); + export interface SidenavLayoutRouteOptions { sidenav?: boolean; } -export const SIDENAV_LAYOUT_CONFIGURATION: LayoutConfiguration = - { - component: SidenavLayout, - routes: [ - { - path: ROUTE_PATHS.V03_COMMON, - exact: true, - component: CommonPage_v03, - }, - ], - }; +export const SIDENAV_LAYOUT_CONFIGURATION: LayoutConfiguration = { + component: SidenavLayout, + routes: [ + { + path: ROUTE_PATHS.V03_COMMON, + exact: true, + component: CommonPage_v03, + }, + ], +}; 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..5bd1d392da 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(); @@ -252,6 +261,7 @@ export default function ChatComponent({ const [shouldReinitializeEditor, setShouldReinitializeEditor] = useState(false); const onClear = () => { + textInputRef?.current?.clear?.(); setShouldReinitializeEditor(true); setMessage(parseStringToTextEditorValue()); }; @@ -260,6 +270,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 +401,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 +572,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 +619,7 @@ export default function ChatComponent({ discussionMessages, isChatChannel, linkPreviewData, + isOptimisticChat, ], ); @@ -719,6 +721,11 @@ export default function ChatComponent({ } }, [discussionMessageReply, currentFilesPreview]); + useLayoutEffect(() => { + textInputRef?.current?.clear?.(); + textInputRef?.current?.focus?.(); + },[discussionId]); + useEffect(() => { if (isFetchedDiscussionMessages) { onMessagesAmountChange?.(discussionMessages.length); @@ -853,6 +860,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..ed66caaa2b 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -5,13 +5,14 @@ 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 copyToClipboard from "copy-to-clipboard"; 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, ShareButtonText } from "@/shared/constants"; import { useModal, useNotification } from "@/shared/hooks"; import { FeedItemFollowState, @@ -33,10 +34,9 @@ import { PredefinedTypes, } from "@/shared/models"; import { TextEditorValue } from "@/shared/ui-kit"; -import { StaticLinkType, getUserName, InternalLinkData } from "@/shared/utils"; +import { StaticLinkType, getUserName, InternalLinkData, generateFirstMessage, generateStaticShareLink } from "@/shared/utils"; import { useChatContext } from "../ChatComponent"; import { FeedCard } from "../FeedCard"; -import { FeedCardShare } from "../FeedCard"; import { FeedItemRef, GetLastMessageOptions, @@ -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,17 +115,13 @@ function DiscussionFeedCard(props, ref) { onUserClick, onFeedItemClick, onInternalLinkClick, + isOptimisticallyCreated, } = props; const { isShowing: isReportModalOpen, onOpen: onReportModalOpen, onClose: onReportModalClose, } = useModal(false); - const { - isShowing: isShareModalOpen, - onOpen: onShareModalOpen, - onClose: onShareModalClose, - } = useModal(false); const { isShowing: isDeleteModalOpen, onOpen: onDeleteModalOpen, @@ -187,10 +186,16 @@ function DiscussionFeedCard(props, ref) { getNonAllowedItems, feedItemUserMetadata, withoutMenu, + shareText: ShareButtonText.Stream }, { report: onReportModalOpen, - share: () => onShareModalOpen(), + share: () => { + if(discussion) { + copyToClipboard(generateStaticShareLink(StaticLinkType.Discussion, discussion, item.id)); + notify("The link has been copied!"); + } + }, remove: onDeleteModalOpen, linkStream: onLinkStreamModalOpen, unlinkStream: onUnlinkStreamModalOpen, @@ -213,6 +218,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 +258,7 @@ function DiscussionFeedCard(props, ref) { feedItemUserMetadata?.hasUnseenMention, nestedItemData, isPreviewMode, + isActive, ]); const onDiscussionDelete = useCallback(async () => { @@ -273,6 +285,13 @@ function DiscussionFeedCard(props, ref) { [preloadDiscussionMessagesData.preloadDiscussionMessages], ); + useEffect(() => { + if(item.data.lastMessage?.content && discussion?.id && isOptimisticallyCreated) { + markFeedItemAsSeen({feedObjectId: item.id, commonId}) + setTimeout(() => dispatch(commonActions.clearCreatedOptimisticFeedItem(discussion?.id)), 10000); + } + },[item.id, item.data.lastMessage?.content, discussion?.id, isOptimisticallyCreated, commonId]) + useEffect(() => { fetchDiscussionCreator(item.userId); }, [item.userId]); @@ -347,12 +366,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 +396,7 @@ function DiscussionFeedCard(props, ref) { isProject, item.data.hasFiles, item.data.hasImages, + isOptimisticallyCreated, ]); return ( @@ -390,13 +419,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} @@ -436,15 +465,6 @@ function DiscussionFeedCard(props, ref) { type={EntityTypes.Discussion} /> )} - {discussion && ( - - )} {isDeleteModalOpen && ( , }, diff --git a/src/pages/common/components/DiscussionFeedCard/utils/getAllowedItems.ts b/src/pages/common/components/DiscussionFeedCard/utils/getAllowedItems.ts index 27f3e77cb0..ac8573a881 100644 --- a/src/pages/common/components/DiscussionFeedCard/utils/getAllowedItems.ts +++ b/src/pages/common/components/DiscussionFeedCard/utils/getAllowedItems.ts @@ -31,26 +31,18 @@ const MENU_ITEM_TO_CHECK_FUNCTION_MAP: Record< ); }, [FeedItemMenuItem.MarkUnread]: ({ feedItemUserMetadata }) => { - const { count, seen, isSeenUpdating } = feedItemUserMetadata || {}; + const { count, seen } = feedItemUserMetadata || {}; if (!feedItemUserMetadata) { return true; } - if (isSeenUpdating) { - return false; - } - return notEmpty(count) && notEmpty(seen) && count === 0 && seen; }, [FeedItemMenuItem.MarkRead]: ({ feedItemUserMetadata }) => { - const { count, seenOnce, seen, isSeenUpdating } = + const { count, seenOnce, seen } = feedItemUserMetadata || {}; - if (isSeenUpdating) { - return false; - } - return ( Boolean(count) || (notEmpty(seen) && !seen) || 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..a8d4602561 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,7 +35,7 @@ export const FeedCardTags: FC = (props) => { const isOwner = ownerId === user?.uid; const isNewTagVisible = notEmpty(seenOnce) && notEmpty(isOwner) && !seenOnce && !isOwner; - const isUnseenTagVisible = + const isUnseenTagVisible = !isNewTagVisible && !unreadMessages && notEmpty(seen) && !seen; return ( @@ -86,3 +86,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/FeedItem/types.ts b/src/pages/common/components/FeedItem/types.ts index 39e24d71d4..c00a97c579 100644 --- a/src/pages/common/components/FeedItem/types.ts +++ b/src/pages/common/components/FeedItem/types.ts @@ -9,6 +9,7 @@ import { CommonFeedObjectUserUnique, } from "@/shared/models"; import { FeedItemMenuItem } from "./constants"; +import { ShareButtonText } from "@/shared/constants"; export type GetNonAllowedItemsOptions = ( type: CommonFeedType, @@ -26,6 +27,7 @@ export interface GetAllowedItemsOptions { getNonAllowedItems?: GetNonAllowedItemsOptions; feedItemUserMetadata: CommonFeedObjectUserUnique | null; withoutMenu?: boolean; + shareText?: ShareButtonText; } export type MenuItemOptions = Omit; 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/ProposalFeedCard/ProposalFeedCard.tsx b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx index 8039066b84..050ddf0b9d 100644 --- a/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx +++ b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx @@ -8,11 +8,12 @@ import React, { import { useSelector } from "react-redux"; import { useUpdateEffect } from "react-use"; import { debounce } from "lodash"; +import copyToClipboard from "copy-to-clipboard"; import { selectUser } from "@/pages/Auth/store/selectors"; import { useCommonMember, useProposalUserVote } from "@/pages/OldCommon/hooks"; import { ProposalService } from "@/services"; import { DeletePrompt, GlobalOverlay } from "@/shared/components"; -import { InboxItemType } from "@/shared/constants"; +import { InboxItemType, ShareButtonText } from "@/shared/constants"; import { useRoutesContext } from "@/shared/contexts"; import { useForceUpdate, useModal, useNotification } from "@/shared/hooks"; import { @@ -34,10 +35,10 @@ import { PredefinedTypes, } from "@/shared/models"; import { TextEditorValue } from "@/shared/ui-kit"; -import { StaticLinkType, getUserName, InternalLinkData } from "@/shared/utils"; +import { StaticLinkType, getUserName, InternalLinkData, generateStaticShareLink } from "@/shared/utils"; import { useChatContext } from "../ChatComponent"; import { useMenuItems } from "../DiscussionFeedCard/hooks"; -import { FeedCard, FeedCardShare } from "../FeedCard"; +import { FeedCard } from "../FeedCard"; import { FeedItemRef, GetLastMessageOptions, @@ -173,11 +174,6 @@ const ProposalFeedCard = forwardRef( setHovering(isMouseEnter); }; const proposalId = item.data.id; - const { - isShowing: isShareModalOpen, - onOpen: onShareModalOpen, - onClose: onShareModalClose, - } = useModal(false); const preloadDiscussionMessagesData = usePreloadDiscussionMessagesById({ commonId, discussionId: discussion?.id, @@ -199,10 +195,16 @@ const ProposalFeedCard = forwardRef( getNonAllowedItems, feedItemUserMetadata, withoutMenu, + shareText: ShareButtonText.Stream }, { report: () => {}, - share: () => onShareModalOpen(), + share: () => { + if(discussion) { + copyToClipboard(generateStaticShareLink(StaticLinkType.Proposal, discussion, item.id)); + notify("The link has been copied!"); + } + }, remove: onProposalDeleteModalOpen, markFeedItemAsSeen, markFeedItemAsUnseen, @@ -457,15 +459,6 @@ const ProposalFeedCard = forwardRef( /> )} - {discussion && ( - - )} {isProposalDeleteModalOpen && ( = (props) => { const advancedSettings: SpaceAdvancedSettingsIntermediate = useMemo(() => { const initialCircles = Object.values(governance?.circles || {}); + const sortedRootCommonRoles = sortByTierDesc(rootCommonRoles); + + const sortedParentCommonRoles = sortByTierDesc(parentCommonRoles); + + const circlesWithInheritRoles = sortedRootCommonRoles.map((rootCommonRole, index) => { + const initialCircle = initialCircles.find( + (circle) => circle.derivedFrom?.circleId === rootCommonRole.circleId, + ); + + const isSelected = Boolean(!isEditing || initialCircle); + const roleForInheritance = + sortedParentCommonRoles.slice(0,2).find( + (parentRole) => parentRole.circleId === initialCircle?.inheritFrom?.circleId, + ) || sortedParentCommonRoles[index]; + + + return { + circleId: rootCommonRole.circleId, + circleName: `${rootCommonRole.circleName}s`, + selected: isSelected, + synced: Boolean( + isEditing ? initialCircle?.inheritGovernanceId : roleForInheritance ? true : false, + ), + ...(parentGovernanceId && + roleForInheritance && { + inheritFrom: { + governanceId: parentGovernanceId, + circleId: roleForInheritance.circleId, + circleName: `${roleForInheritance.circleName}s`, + tier: roleForInheritance.tier, + }, + }), + }; + }); + return { permissionGovernanceId: isParentIsRoot ? parentGovernanceId : rootGovernance?.id, - circles: rootCommonRoles.map((rootCommonRole, index) => { - const initialCircle = initialCircles.find( - (circle) => circle.derivedFrom?.circleId === rootCommonRole.circleId, - ); - const isSelected = Boolean(!isEditing || initialCircle); - const roleForInheritance = - parentCommonRoles.find( - (parentRole) => - parentRole.circleId === initialCircle?.inheritFrom?.circleId, - ) || parentCommonRoles[index]; - - return { - circleId: rootCommonRole.circleId, - circleName: `${rootCommonRole.circleName}s`, - selected: isSelected, - synced: Boolean( - isEditing ? initialCircle?.inheritGovernanceId : index === 0, - ), - ...(parentGovernanceId && - roleForInheritance && { - inheritFrom: { - governanceId: parentGovernanceId, - circleId: roleForInheritance.circleId, - circleName: `${roleForInheritance.circleName}s`, - tier: roleForInheritance.tier, - }, - }), - }; - }), + circles: circlesWithInheritRoles.reverse(), }; }, [ rootGovernance?.id, 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 { common, commonMember, commonFollow, onClick, onSearchClick } = props; - const { - isShowing: isShareModalOpen, - onOpen: onShareModalOpen, - onClose: onShareModalClose, - } = useModal(false); + const { notify } = useNotification(); const { markCommonAsSeen } = useUpdateCommonSeenState(); + const shareLink = generateStaticShareLink(StaticLinkType.Common, common); const items = useMenuItems( { common, commonMember, isFollowInProgress: commonFollow.isFollowInProgress, isSearchActionAvailable: Boolean(onSearchClick), + shareText: ShareButtonText.Space, }, { - share: onShareModalOpen, + share: () => { + copyToClipboard(shareLink); + notify("The link has been copied!"); + }, onFollowToggle: commonFollow.onFollowToggle, onSearchClick, markCommonAsSeen }, ); - const shareLink = generateStaticShareLink(StaticLinkType.Common, common); + const triggerEl = ; return ( <> {items.length > 0 && } - ); }; diff --git a/src/pages/commonFeed/components/HeaderContent/components/ActionsButton/hooks/useMenuItems.tsx b/src/pages/commonFeed/components/HeaderContent/components/ActionsButton/hooks/useMenuItems.tsx index e36c7305e7..d8b72dc396 100644 --- a/src/pages/commonFeed/components/HeaderContent/components/ActionsButton/hooks/useMenuItems.tsx +++ b/src/pages/commonFeed/components/HeaderContent/components/ActionsButton/hooks/useMenuItems.tsx @@ -25,7 +25,7 @@ export const useMenuItems = ( options: GetAllowedItemsOptions, actions: Actions, ): Item[] => { - const { common } = options; + const { common, shareText } = options; const { share, onFollowToggle, onSearchClick, markCommonAsSeen } = actions; const items: Item[] = [ @@ -37,7 +37,7 @@ export const useMenuItems = ( }, { id: CommonFeedMenuItem.Share, - text: "Share", + text: shareText ?? "Share", onClick: share, icon: , }, diff --git a/src/pages/commonFeed/components/HeaderContent/components/NewStreamButton/NewStreamButton.tsx b/src/pages/commonFeed/components/HeaderContent/components/NewStreamButton/NewStreamButton.tsx index 07ceab3e39..e705ea06fa 100644 --- a/src/pages/commonFeed/components/HeaderContent/components/NewStreamButton/NewStreamButton.tsx +++ b/src/pages/commonFeed/components/HeaderContent/components/NewStreamButton/NewStreamButton.tsx @@ -1,8 +1,12 @@ import React, { FC } from "react"; import { useHistory } from "react-router"; +import { useDispatch } from "react-redux"; import { useMenuItems } from "@/pages/common/components/CommonTabPanels/components/FeedTab/components/NewStreamButton"; import { useRoutesContext } from "@/shared/contexts"; import { PlusIcon } from "@/shared/icons"; +import { animateScroll } from "react-scroll"; +import { CommonAction } from "@/shared/constants"; +import { commonActions } from "@/store/states"; import { CirclesPermissions, CommonMember, Governance } from "@/shared/models"; import { ButtonIcon, DesktopMenu, MobileMenu } from "@/shared/ui-kit"; import styles from "./NewStreamButton.module.scss"; @@ -29,6 +33,12 @@ const NewStreamButton: FC = (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/pages/commonFeed/utils/getAllowedItems.ts b/src/pages/commonFeed/utils/getAllowedItems.ts index 8be993be32..4f3d1a3e66 100644 --- a/src/pages/commonFeed/utils/getAllowedItems.ts +++ b/src/pages/commonFeed/utils/getAllowedItems.ts @@ -1,12 +1,14 @@ import { MenuItem as Item } from "@/shared/interfaces"; import { CirclesPermissions, Common, CommonMember } from "@/shared/models"; import { CommonFeedMenuItem } from "../constants"; +import { ShareButtonText } from "@/shared/constants"; export interface Options { common: Common; commonMember: (CommonMember & CirclesPermissions) | null; isFollowInProgress: boolean; isSearchActionAvailable: boolean; + shareText?: ShareButtonText; } const MENU_ITEM_TO_CHECK_FUNCTION_MAP: Record< diff --git a/src/pages/inbox/components/ChatChannelItem/utils/getAllowedItems.ts b/src/pages/inbox/components/ChatChannelItem/utils/getAllowedItems.ts index b2ed935f22..e3dbbb81f5 100644 --- a/src/pages/inbox/components/ChatChannelItem/utils/getAllowedItems.ts +++ b/src/pages/inbox/components/ChatChannelItem/utils/getAllowedItems.ts @@ -12,23 +12,17 @@ const MENU_ITEM_TO_CHECK_FUNCTION_MAP: Record< (options: Options) => boolean > = { [ChatChannelMenuItem.MarkUnread]: ({ chatChannelUserStatus }) => { - const { notSeenCount, seen, isSeenUpdating } = chatChannelUserStatus || {}; + const { notSeenCount, seen } = chatChannelUserStatus || {}; - if (isSeenUpdating) { - return false; - } return ( notEmpty(notSeenCount) && notEmpty(seen) && notSeenCount === 0 && seen ); }, [ChatChannelMenuItem.MarkRead]: ({ chatChannelUserStatus }) => { - const { notSeenCount, seenOnce, seen, isSeenUpdating } = + const { notSeenCount, seenOnce, seen } = chatChannelUserStatus || {}; - if (isSeenUpdating) { - return false; - } return ( Boolean(notSeenCount) || diff --git a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx index 8b4b56a0bf..204f5a5964 100644 --- a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx +++ b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx @@ -121,6 +121,7 @@ const ChatMessage = ({ }>(); const [isEditMode, setEditMode] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); + const [parsedMessage, setParsedMessage] = useState(discussionMessage.parsedText); const [isMessageEditLoading, setIsMessageEditLoading] = useState(false); const isTabletView = useIsTabletView(); const isUserDiscussionMessage = @@ -133,6 +134,9 @@ const ChatMessage = ({ : null; const isNotCurrentUserMessage = !isUserDiscussionMessage || userId !== discussionMessageUserId; + const initialEditedAtDate = new Date( + (discussionMessage.editedAt?.seconds ?? 0) * 1000, + ); const [replyMessageText, setReplyMessageText] = useState< (string | JSX.Element)[] @@ -177,9 +181,7 @@ const ChatMessage = ({ ]); const createdAtDate = new Date(discussionMessage.createdAt.seconds * 1000); - const editedAtDate = new Date( - (discussionMessage.editedAt?.seconds ?? 0) * 1000, - ); + const [editedAtDate, setEditedAtDate] = useState(initialEditedAtDate); const handleUserClick = () => { if (onUserClick && discussionMessageUserId && !isBotMessage) { @@ -258,6 +260,8 @@ const ChatMessage = ({ handleEditModeClose(); } else { notify("Something went wrong"); + setParsedMessage(discussionMessage.parsedText); + setEditedAtDate(initialEditedAtDate); } }, }), @@ -276,17 +280,38 @@ const ChatMessage = ({ handleEditModeClose(); } catch (err) { notify("Something went wrong"); + setParsedMessage(discussionMessage.parsedText); + setEditedAtDate(initialEditedAtDate); } finally { setIsMessageEditLoading(false); } }; - const updateMessage = (message: TextEditorValue) => { + const updateMessage = async (message: TextEditorValue) => { + try { if (chatType === ChatType.ChatMessages) { updateChatMessage(message); } else { updateDiscussionMessage(message); } + const parsedText = await getTextFromTextEditorString({ + userId, + ownerId: discussionMessageUserId, + textEditorString: JSON.stringify(message), + users, + commonId: discussionMessage.commonId, + directParent, + onUserClick, + onFeedItemClick, + onInternalLinkClick, + }); + + setParsedMessage(parsedText); + setEditedAtDate(new Date()); + handleEditModeClose(); + } catch(err) { + setIsMessageEditLoading(false); + } }; updateMessageRef.current = { updateMessage, @@ -520,7 +545,7 @@ const ChatMessage = ({ - {discussionMessage.parsedText.map((text) => text)} + {(parsedMessage ?? []).map((text) => text)} {!isSystemMessage && (