diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestCreating.tsx b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestCreating.tsx index cc45178b9a..945356a834 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestCreating.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestCreating.tsx @@ -9,7 +9,8 @@ import { createMemberAdmittanceProposal } from "../../../store/actions"; import { IStageProps } from "./MembershipRequestModal"; import { MembershipRequestStage } from "./constants"; -interface MembershipRequestCreatingProps extends IStageProps { +interface MembershipRequestCreatingProps + extends Omit { shouldSkipCreation?: boolean; } diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestIntroduce.tsx b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestIntroduce.tsx index b7ce6b3b09..56e4cc4274 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestIntroduce.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestIntroduce.tsx @@ -9,6 +9,7 @@ import { getScreenSize } from "@/shared/store/selectors"; import { parseLinksForSubmission } from "@/shared/utils"; import { IStageProps } from "./MembershipRequestModal"; import { MembershipRequestStage } from "./constants"; +import { shouldShowPaymentStep, shouldShowRulesStep } from "./helpers"; import { introduceStageSchema } from "./validationSchemas"; import "./index.scss"; @@ -29,12 +30,16 @@ export default function MembershipRequestIntroduce(props: IStageProps) { const handleSubmit = useCallback["onSubmit"]>( (values) => { - const areRulesSpecified = - governance && governance.unstructuredRules.length > 0; - const nextStage = areRulesSpecified - ? MembershipRequestStage.Rules - : MembershipRequestStage.Payment; const links = parseLinksForSubmission(values.links); + let nextStage: MembershipRequestStage; + + if (shouldShowRulesStep(governance)) { + nextStage = MembershipRequestStage.Rules; + } else { + nextStage = shouldShowPaymentStep(governance) + ? MembershipRequestStage.Payment + : MembershipRequestStage.Creating; + } setUserData((nextUserData) => ({ ...nextUserData, diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestModal.tsx b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestModal.tsx index 15de4c460f..f9c980749a 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestModal.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestModal.tsx @@ -24,13 +24,14 @@ import MembershipRequestProgressBar from "./MembershipRequestProgressBar"; import MembershipRequestRules from "./MembershipRequestRules"; import MembershipRequestWelcome from "./MembershipRequestWelcome"; import { MembershipRequestStage } from "./constants"; +import { getSteps } from "./helpers"; import "./index.scss"; export interface IStageProps { setUserData: Dispatch>; userData: IMembershipRequestData; common?: Common; - governance?: Governance; + governance: Governance; isAutomaticAcceptance?: boolean; } @@ -50,7 +51,7 @@ const INIT_DATA: IMembershipRequestData = { interface IProps extends Pick { common: Common; governance: Governance; - shouldShowLoadingAfterSuccessfulCreation?: boolean; + showLoadingAfterSuccessfulCreation?: boolean; onCreationStageReach?: (reached: boolean) => void; onRequestCreated?: () => void; } @@ -68,7 +69,7 @@ export function MembershipRequestModal(props: IProps) { onClose, common, governance, - shouldShowLoadingAfterSuccessfulCreation = false, + showLoadingAfterSuccessfulCreation = false, onCreationStageReach, onRequestCreated, } = props; @@ -81,15 +82,21 @@ export function MembershipRequestModal(props: IProps) { } = useMemberInAnyCommon(); const user = useSelector(selectUser()); const userId = user?.uid; - const shouldDisplayProgressBar = - stage > MembershipRequestStage.Welcome && - stage < MembershipRequestStage.Creating; const shouldDisplayGoBack = (stage > MembershipRequestStage.Introduce && stage < MembershipRequestStage.Creating) || (stage === MembershipRequestStage.Introduce && !isMember); const isAutomaticAcceptance = checkIsAutomaticJoin(governance); + const steps = useMemo(() => { + return getSteps(governance); + }, [governance]); + + const shouldDisplayProgressBar = + stage > MembershipRequestStage.Welcome && + stage < MembershipRequestStage.Creating && + steps.length > 1; + useEffect(() => { if (isShowing) { disableZoom(); @@ -182,7 +189,7 @@ export function MembershipRequestModal(props: IProps) { /> ); case MembershipRequestStage.Created: - return shouldShowLoadingAfterSuccessfulCreation ? ( + return showLoadingAfterSuccessfulCreation && isAutomaticAcceptance ? (
{shouldDisplayProgressBar && ( - + )} {renderCurrentStage(stage)}
diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestPayment.tsx b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestPayment.tsx index f892945050..fc5b4519a2 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestPayment.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestPayment.tsx @@ -36,7 +36,7 @@ export default function MembershipRequestPayment( !changePaymentMethodState.isPaymentLoading; const contributionInfo = useMemo(() => { - const limitations = governance?.proposals?.MEMBER_ADMITTANCE?.limitations; + const limitations = governance.proposals?.MEMBER_ADMITTANCE?.limitations; if (limitations?.minFeeMonthly) { return { diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestProgressBar.tsx b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestProgressBar.tsx index 7d1c1aebf0..87f2464554 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestProgressBar.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestProgressBar.tsx @@ -8,34 +8,17 @@ import "./index.scss"; interface IProps { currentStage: MembershipRequestStage; + steps: StepProgressItem[]; } -const STEPS: StepProgressItem[] = [ - { - title: "Introduce", - activeImageSource: "/icons/membership-request/introduce-current.svg", - inactiveImageSource: "/icons/membership-request/introduce-current.svg", - }, - { - title: "Rules", - activeImageSource: "/icons/membership-request/rules-current.svg", - inactiveImageSource: "/icons/membership-request/rules-gray.svg", - }, - { - title: "Payment", - activeImageSource: "/icons/membership-request/payment-current.svg", - inactiveImageSource: "/icons/membership-request/payment-gray.svg", - }, -]; - export default function MembershipRequestProgressBar(props: IProps) { - const { currentStage } = props; + const { currentStage, steps } = props; return ( ); } diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestRules.tsx b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestRules.tsx index 4e48cd9875..579802c3b0 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestRules.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestRules.tsx @@ -2,13 +2,12 @@ import React from "react"; import { Button } from "@/shared/components"; import { IStageProps } from "./MembershipRequestModal"; import { MembershipRequestStage } from "./constants"; +import { shouldShowPaymentStep } from "./helpers"; import "./index.scss"; export default function MembershipRequestRules(props: IStageProps) { const { userData, setUserData, governance, isAutomaticAcceptance } = props; - const rules = governance?.unstructuredRules || []; - const paymentMustGoThrough = - governance?.proposals?.MEMBER_ADMITTANCE?.limitations.paymentMustGoThrough; + const rules = governance.unstructuredRules || []; return (
@@ -42,7 +41,7 @@ export default function MembershipRequestRules(props: IStageProps) { onClick={() => setUserData({ ...userData, - stage: paymentMustGoThrough + stage: shouldShowPaymentStep(governance) ? MembershipRequestStage.Payment : MembershipRequestStage.Creating, }) diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestWelcome.tsx b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestWelcome.tsx index a4364c36c5..56d96c30b6 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestWelcome.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/MembershipRequestWelcome.tsx @@ -8,7 +8,9 @@ import { IStageProps } from "./MembershipRequestModal"; import { MembershipRequestStage } from "./constants"; import "./index.scss"; -export default function MembershipRequestWelcome(props: IStageProps) { +export default function MembershipRequestWelcome( + props: Omit, +) { const screenSize = useSelector(getScreenSize()); const isMobileView = screenSize === ScreenSize.Mobile; const { userData, setUserData } = props; diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/constants.ts b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/constants.ts index 34e0cfb2ac..3c2a1fd0ad 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/constants.ts +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/constants.ts @@ -1,3 +1,5 @@ +import { StepProgressItem } from "@/shared/components"; + export enum MembershipRequestStage { Welcome, Introduce, @@ -6,3 +8,27 @@ export enum MembershipRequestStage { Creating, Created, } + +export enum MembershipRequestStep { + Introduce = "Introduce", + Rules = "Rules", + Payment = "Payment", +} + +export const STEPS: StepProgressItem[] = [ + { + title: MembershipRequestStep.Introduce, + activeImageSource: "/icons/membership-request/introduce-current.svg", + inactiveImageSource: "/icons/membership-request/introduce-current.svg", + }, + { + title: MembershipRequestStep.Rules, + activeImageSource: "/icons/membership-request/rules-current.svg", + inactiveImageSource: "/icons/membership-request/rules-gray.svg", + }, + { + title: MembershipRequestStep.Payment, + activeImageSource: "/icons/membership-request/payment-current.svg", + inactiveImageSource: "/icons/membership-request/payment-gray.svg", + }, +]; diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/helpers.ts b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/helpers.ts new file mode 100644 index 0000000000..dc2a854c8d --- /dev/null +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembershipRequestModal/helpers.ts @@ -0,0 +1,29 @@ +import { StepProgressItem } from "@/shared/components"; +import { ProposalsTypes } from "@/shared/constants"; +import { Governance } from "@/shared/models"; +import { STEPS, MembershipRequestStep } from "./constants"; + +export const shouldShowRulesStep = (governance: Governance): boolean => + governance.unstructuredRules.length > 0; + +export const shouldShowPaymentStep = (governance: Governance): boolean => + Boolean( + governance.proposals[ProposalsTypes.MEMBER_ADMITTANCE]?.limitations + .paymentMustGoThrough, + ); + +export const getSteps = (governance: Governance): StepProgressItem[] => { + const stepsToExclude: MembershipRequestStep[] = []; + + if (!shouldShowRulesStep(governance)) { + stepsToExclude.push(MembershipRequestStep.Rules); + } + + if (!shouldShowPaymentStep(governance)) { + stepsToExclude.push(MembershipRequestStep.Payment); + } + + return STEPS.filter( + ({ title }) => !stepsToExclude.includes(title as MembershipRequestStep), + ); +}; diff --git a/src/pages/common/components/ChatComponent/ChatComponent.module.scss b/src/pages/common/components/ChatComponent/ChatComponent.module.scss index 54bcb33c17..f072d86ded 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.module.scss +++ b/src/pages/common/components/ChatComponent/ChatComponent.module.scss @@ -97,6 +97,7 @@ flex-direction: column; justify-content: center; padding: 0.125rem 1.5rem 0.125rem 0.25rem; + word-break: break-word; } .messageInputEmpty { diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 09b0438dbf..a6da5e4335 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -15,6 +15,7 @@ import { delay, omit } from "lodash"; import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { ChatService, DiscussionMessageService, FileService } from "@/services"; +import { InternalLinkData } from "@/shared/components"; import { ChatType, DiscussionMessageOwnerType, @@ -80,7 +81,7 @@ interface ChatComponentInterface { governanceCircles?: Circles; commonMember: CommonMember | null; hasAccess?: boolean; - discussion: Discussion; + discussion?: Discussion; chatChannel?: ChatChannel; lastSeenItem?: CommonFeedObjectUserUnique["lastSeen"]; feedItemId: string; @@ -91,6 +92,8 @@ interface ChatComponentInterface { isJoinPending?: boolean; onJoinCommon?: () => void; onUserClick?: (userId: string) => void; + onFeedItemClick?: (feedItemId: string) => void; + onInternalLinkClick?: (data: InternalLinkData) => void; } interface Messages { @@ -131,6 +134,8 @@ export default function ChatComponent({ isJoinPending = false, onJoinCommon, onUserClick, + onFeedItemClick, + onInternalLinkClick, }: ChatComponentInterface) { const dispatch = useDispatch(); useZoomDisabling(); @@ -142,7 +147,7 @@ export default function ChatComponent({ ); const user = useSelector(selectUser()); const userId = user?.uid; - const discussionId = discussion.id; + const discussionId = discussion?.id || ""; const isChatChannel = Boolean(chatChannel); const hasPermissionToHide = @@ -204,9 +209,9 @@ export default function ChatComponent({ useEffect(() => { if (commonId && !isChatChannel) { - fetchDiscussionUsers(commonId, discussion.circleVisibility); + fetchDiscussionUsers(commonId, discussion?.circleVisibility); } - }, [commonId, discussion.circleVisibility]); + }, [commonId, discussion?.circleVisibility]); useEffect(() => { if (chatChannel?.id) { @@ -602,10 +607,12 @@ export default function ChatComponent({ users={users} discussionId={discussionId} feedItemId={feedItemId} - isLoading={isLoadingDiscussionMessages} + isLoading={!discussion || isLoadingDiscussionMessages} onMessageDelete={handleMessageDelete} directParent={directParent} onUserClick={onUserClick} + onFeedItemClick={onFeedItemClick} + onInternalLinkClick={onInternalLinkClick} />
{isAuthorized && ( diff --git a/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx b/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx index babe51dd78..a05f405fcd 100644 --- a/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx +++ b/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx @@ -13,7 +13,7 @@ import { v4 as uuidv4 } from "uuid"; import { selectUser } from "@/pages/Auth/store/selectors"; import { EmptyTabComponent } from "@/pages/OldCommon/components/CommonDetailContainer"; import { Loader } from "@/shared/components"; -import { ChatMessage } from "@/shared/components"; +import { ChatMessage, InternalLinkData } from "@/shared/components"; import { ChatType, QueryParamKey } from "@/shared/constants"; import { useQueryParams } from "@/shared/hooks"; import { @@ -54,6 +54,8 @@ interface ChatContentInterface { onMessageDelete?: (messageId: string) => void; directParent?: DirectParent | null; onUserClick?: (userId: string) => void; + onFeedItemClick?: (feedItemId: string) => void; + onInternalLinkClick?: (data: InternalLinkData) => void; } const isToday = (someDate: Date) => { @@ -89,21 +91,19 @@ const ChatContent: ForwardRefRenderFunction< onMessageDelete, directParent, onUserClick, + onFeedItemClick, + onInternalLinkClick, }, chatContentRef, ) => { const user = useSelector(selectUser()); const userId = user?.uid; const queryParams = useQueryParams(); + const messageIdParam = queryParams[QueryParamKey.Message]; - const [highlightedMessageId, setHighlightedMessageId] = useState(() => { - const sharedMessageIdQueryParam = queryParams[QueryParamKey.Message]; - return ( - (typeof sharedMessageIdQueryParam === "string" && - sharedMessageIdQueryParam) || - null - ); - }); + const [highlightedMessageId, setHighlightedMessageId] = useState( + () => (typeof messageIdParam === "string" && messageIdParam) || null, + ); const [scrolledToMessage, setScrolledToMessage] = useState(false); @@ -175,6 +175,12 @@ const ChatContent: ForwardRefRenderFunction< setHighlightedMessageId(messageId); } + useEffect(() => { + if (typeof messageIdParam === "string") { + setHighlightedMessageId(messageIdParam); + } + }, [messageIdParam]); + useImperativeHandle( chatContentRef, () => ({ @@ -266,6 +272,8 @@ const ChatContent: ForwardRefRenderFunction< onMessageDelete={onMessageDelete} directParent={directParent} onUserClick={onUserClick} + onFeedItemClick={onFeedItemClick} + onInternalLinkClick={onInternalLinkClick} /> ); diff --git a/src/pages/common/components/ChatComponent/context.ts b/src/pages/common/components/ChatComponent/context.ts index befe8c2fd0..518c6ba7b4 100644 --- a/src/pages/common/components/ChatComponent/context.ts +++ b/src/pages/common/components/ChatComponent/context.ts @@ -9,7 +9,7 @@ import { export interface ChatItem { feedItemId: string; proposal?: Proposal; - discussion: Discussion; + discussion?: Discussion; chatChannel?: ChatChannel; circleVisibility: string[]; lastSeenItem?: CommonFeedObjectUserUnique["lastSeen"]; diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/FeedTab.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/FeedTab.tsx index f8c1ecffaa..ddc2b5404a 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/FeedTab.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/FeedTab.tsx @@ -114,11 +114,11 @@ export const FeedTab: FC = (props) => {

- {chatItem.discussion.title} + {chatItem.discussion?.title}

= (props) => { }} commonName={common.name} commonImage={common.image} - title={chatItem?.discussion.title} + title={chatItem?.discussion?.title} > {chatItem && ( = (props) => { const contextValue = useMemo( () => ({ setChatItem, - activeItemDiscussionId: chatItem?.discussion.id, + activeItemDiscussionId: chatItem?.discussion?.id, }), - [setChatItem, chatItem?.discussion.id], + [setChatItem, chatItem?.discussion?.id], ); return ( diff --git a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx index 8381d712a2..cc7951603e 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -1,4 +1,10 @@ -import React, { FC, ReactNode, useCallback, useEffect, useState } from "react"; +import React, { + forwardRef, + ReactNode, + useCallback, + useEffect, + useState, +} from "react"; import { useSelector } from "react-redux"; import { selectUser } from "@/pages/Auth/store/selectors"; import { DiscussionService } from "@/services"; @@ -16,7 +22,6 @@ import { FeedLayoutItemChangeData } from "@/shared/interfaces"; import { Common, CommonFeed, - CommonLink, CommonMember, DirectParent, Governance, @@ -33,7 +38,11 @@ import { } from "../FeedCard"; import { getVisibilityString } from "../FeedCard"; import { FeedCardShare } from "../FeedCard"; -import { GetLastMessageOptions, GetNonAllowedItemsOptions } from "../FeedItem"; +import { + FeedItemRef, + GetLastMessageOptions, + GetNonAllowedItemsOptions, +} from "../FeedItem"; import { useMenuItems } from "./hooks"; interface DiscussionFeedCardProps { @@ -57,290 +66,301 @@ interface DiscussionFeedCardProps { onUserSelect?: (userId: string, commonId?: string) => void; } -const DiscussionFeedCard: FC = (props) => { - const { setChatItem, feedItemIdForAutoChatOpen, shouldAllowChatAutoOpen } = - useChatContext(); - const { notify } = useNotification(); - const { - item, - governanceCircles, - isMobileVersion = false, - commonId, - commonName, - commonImage, - pinnedFeedItems, - commonMember, - isProject, - isPinned, - isPreviewMode, - isActive, - isExpanded, - getLastMessage, - getNonAllowedItems, - onActiveItemDataChange, - directParent, - onUserSelect, - } = props; - const { - isShowing: isReportModalOpen, - onOpen: onReportModalOpen, - onClose: onReportModalClose, - } = useModal(false); - const { - isShowing: isShareModalOpen, - onOpen: onShareModalOpen, - onClose: onShareModalClose, - } = useModal(false); - const { - isShowing: isDeleteModalOpen, - onOpen: onDeleteModalOpen, - onClose: onDeleteModalClose, - } = useModal(false); - const [isDeletingInProgress, setDeletingInProgress] = useState(false); - const { - fetchUser: fetchDiscussionCreator, - data: discussionCreator, - fetched: isDiscussionCreatorFetched, - } = useUserById(); - const { - fetchDiscussion, - data: discussion, - fetched: isDiscussionFetched, - } = useDiscussionById(); - const isHome = discussion?.predefinedType === PredefinedTypes.General; - const { - data: feedItemUserMetadata, - fetched: isFeedItemUserMetadataFetched, - fetchFeedItemUserMetadata, - } = useFeedItemUserMetadata(); - const { data: common } = useCommon(isHome ? commonId : ""); - const feedItemFollow = useFeedItemFollow(item.id, commonId); - const menuItems = useMenuItems( - { +const DiscussionFeedCard = forwardRef( + (props, ref) => { + const { setChatItem, feedItemIdForAutoChatOpen, shouldAllowChatAutoOpen } = + useChatContext(); + const { notify } = useNotification(); + const { + item, + governanceCircles, + isMobileVersion = false, commonId, + commonName, + commonImage, pinnedFeedItems, - feedItem: item, - discussion, - governanceCircles, commonMember, - feedItemFollow, + isProject, + isPinned, + isPreviewMode, + isActive, + isExpanded, + getLastMessage, getNonAllowedItems, - }, - { - report: onReportModalOpen, - share: () => onShareModalOpen(), - remove: onDeleteModalOpen, - }, - ); - const user = useSelector(selectUser()); - const [isHovering, setHovering] = useState(false); - const onHover = (isMouseEnter: boolean): void => { - setHovering(isMouseEnter); - }; - const userId = user?.uid; - const isLoading = - !isDiscussionCreatorFetched || - !isDiscussionFetched || - !isFeedItemUserMetadataFetched || - !commonId; - const cardTitle = discussion?.title; - - const handleOpenChat = useCallback(() => { - if (discussion) { - setChatItem({ - feedItemId: item.id, + onActiveItemDataChange, + directParent, + onUserSelect, + } = props; + const { + isShowing: isReportModalOpen, + onOpen: onReportModalOpen, + onClose: onReportModalClose, + } = useModal(false); + const { + isShowing: isShareModalOpen, + onOpen: onShareModalOpen, + onClose: onShareModalClose, + } = useModal(false); + const { + isShowing: isDeleteModalOpen, + onOpen: onDeleteModalOpen, + onClose: onDeleteModalClose, + } = useModal(false); + const [isDeletingInProgress, setDeletingInProgress] = useState(false); + const { + fetchUser: fetchDiscussionCreator, + data: discussionCreator, + fetched: isDiscussionCreatorFetched, + } = useUserById(); + const { + fetchDiscussion, + data: discussion, + fetched: isDiscussionFetched, + } = useDiscussionById(); + const isHome = discussion?.predefinedType === PredefinedTypes.General; + const { + data: feedItemUserMetadata, + fetched: isFeedItemUserMetadataFetched, + fetchFeedItemUserMetadata, + } = useFeedItemUserMetadata(); + const { data: common } = useCommon(isHome ? commonId : ""); + const feedItemFollow = useFeedItemFollow(item.id, commonId); + const menuItems = useMenuItems( + { + commonId, + pinnedFeedItems, + feedItem: item, discussion, - circleVisibility: item.circleVisibility, - lastSeenItem: feedItemUserMetadata?.lastSeen, - lastSeenAt: feedItemUserMetadata?.lastSeenAt, - seenOnce: feedItemUserMetadata?.seenOnce, - }); - } - }, [ - discussion, - item.id, - item.circleVisibility, - feedItemUserMetadata?.lastSeen, - feedItemUserMetadata?.lastSeenAt, - feedItemUserMetadata?.seenOnce, - ]); + governanceCircles, + commonMember, + feedItemFollow, + getNonAllowedItems, + }, + { + report: onReportModalOpen, + share: () => onShareModalOpen(), + remove: onDeleteModalOpen, + }, + ); + const user = useSelector(selectUser()); + const [isHovering, setHovering] = useState(false); + const onHover = (isMouseEnter: boolean): void => { + setHovering(isMouseEnter); + }; + const userId = user?.uid; + const isLoading = + !isDiscussionCreatorFetched || + !isDiscussionFetched || + !isFeedItemUserMetadataFetched || + !commonId; + const cardTitle = discussion?.title; - const onDiscussionDelete = useCallback(async () => { - try { + const handleOpenChat = useCallback(() => { if (discussion) { - setDeletingInProgress(true); - await DiscussionService.deleteDiscussion(discussion.id); - onDeleteModalClose(); + setChatItem({ + feedItemId: item.id, + discussion, + circleVisibility: item.circleVisibility, + lastSeenItem: feedItemUserMetadata?.lastSeen, + lastSeenAt: feedItemUserMetadata?.lastSeenAt, + seenOnce: feedItemUserMetadata?.seenOnce, + }); } - } catch { - notify("Something went wrong"); - } finally { - setDeletingInProgress(false); - } - }, [discussion]); + }, [ + discussion, + item.id, + item.circleVisibility, + feedItemUserMetadata?.lastSeen, + feedItemUserMetadata?.lastSeenAt, + feedItemUserMetadata?.seenOnce, + ]); + + const onDiscussionDelete = useCallback(async () => { + try { + if (discussion) { + setDeletingInProgress(true); + await DiscussionService.deleteDiscussion(discussion.id); + onDeleteModalClose(); + } + } catch { + notify("Something went wrong"); + } finally { + setDeletingInProgress(false); + } + }, [discussion]); - useEffect(() => { - fetchDiscussionCreator(item.userId); - }, [item.userId]); + useEffect(() => { + fetchDiscussionCreator(item.userId); + }, [item.userId]); - useEffect(() => { - fetchDiscussion(item.data.id); - }, [item.data.id]); + useEffect(() => { + fetchDiscussion(item.data.id); + }, [item.data.id]); - useEffect(() => { - if (commonId) { - fetchFeedItemUserMetadata({ - userId: userId || "", - commonId, - feedObjectId: item.id, - }); - } - }, [userId, commonId, item.id]); + useEffect(() => { + if (commonId) { + fetchFeedItemUserMetadata({ + userId: userId || "", + commonId, + feedObjectId: item.id, + }); + } + }, [userId, commonId, item.id]); + + useEffect(() => { + if ( + (!isActive || + shouldAllowChatAutoOpen === null || + shouldAllowChatAutoOpen) && + isDiscussionFetched && + isFeedItemUserMetadataFetched && + item.id === feedItemIdForAutoChatOpen && + !isMobileVersion + ) { + handleOpenChat(); + } + }, [ + isDiscussionFetched, + isFeedItemUserMetadataFetched, + shouldAllowChatAutoOpen, + ]); - useEffect(() => { - if ( - isDiscussionFetched && - isFeedItemUserMetadataFetched && - item.id === feedItemIdForAutoChatOpen && - !isMobileVersion && - shouldAllowChatAutoOpen !== false - ) { - handleOpenChat(); - } - }, [ - isDiscussionFetched, - 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 (isActive && cardTitle) { - onActiveItemDataChange?.({ - itemId: item.id, - title: cardTitle, - }); - } - }, [isActive, cardTitle]); + const renderContent = (): ReactNode => { + if (isLoading) { + return null; + } - const renderContent = (): ReactNode => { - if (isLoading) { - return null; - } + const circleVisibility = governanceCircles + ? getVisibilityString(governanceCircles, discussion?.circleVisibility) + : undefined; - const circleVisibility = governanceCircles - ? getVisibilityString(governanceCircles, discussion?.circleVisibility) - : undefined; + return ( + <> + {!isHome && ( + + Created:{" "} + + + } + type="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); + }} + /> + + ); + }; return ( <> - {!isHome && ( - - Created:{" "} - - - } - type="Discussion" - circleVisibility={circleVisibility} - menuItems={menuItems} - isMobileVersion={isMobileVersion} - commonId={commonId} - userId={item.userId} - directParent={directParent} - onUserSelect={ - onUserSelect && (() => onUserSelect(item.userId, commonId)) - } + + {renderContent()} + + {userId && discussion && ( + )} - { - onHover(true); - }} - onMouseLeave={() => { - onHover(false); - }} - /> + {discussion && ( + + )} + {isDeleteModalOpen && ( + + + + )} ); - }; - - return ( - <> - - {renderContent()} - - {userId && discussion && ( - - )} - {discussion && ( - - )} - {isDeleteModalOpen && ( - - - - )} - - ); -}; + }, +); export default DiscussionFeedCard; diff --git a/src/pages/common/components/FeedCard/FeedCard.tsx b/src/pages/common/components/FeedCard/FeedCard.tsx index 25e3cc3d16..ea660ca610 100644 --- a/src/pages/common/components/FeedCard/FeedCard.tsx +++ b/src/pages/common/components/FeedCard/FeedCard.tsx @@ -1,4 +1,11 @@ -import React, { FC, useEffect, useRef, MouseEventHandler } from "react"; +import React, { + useEffect, + useRef, + MouseEventHandler, + forwardRef, + useImperativeHandle, + PropsWithChildren, +} from "react"; import { useCollapse } from "react-collapsed"; import classNames from "classnames"; import { useFeedItemContext } from "@/pages/common"; @@ -7,9 +14,10 @@ import { ContextMenuItem } from "@/shared/interfaces"; import { CommonFeedType, PredefinedTypes } from "@/shared/models"; import { Loader, TextEditorValue } from "@/shared/ui-kit"; import { CommonCard } from "../CommonCard"; +import { FeedCardRef } from "./types"; import styles from "./FeedCard.module.scss"; -interface FeedCardProps { +type FeedCardProps = PropsWithChildren<{ className?: string; feedItemId: string; isHovering?: boolean; @@ -36,7 +44,7 @@ interface FeedCardProps { discussionPredefinedType?: PredefinedTypes; hasFiles?: boolean; hasImages?: boolean; -} +}>; const MOBILE_HEADER_HEIGHT = 52; const DESKTOP_HEADER_HEIGHT = 72; @@ -45,7 +53,7 @@ const COLLAPSE_DURATION = 300; const OFFSET_FROM_BOTTOM_FOR_SCROLLING = 10; const EXTRA_WAITING_TIME_FOR_TIMEOUT = 10; -export const FeedCard: FC = (props) => { +export const FeedCard = forwardRef((props, ref) => { const { className, feedItemId, @@ -161,6 +169,10 @@ export const FeedCard: FC = (props) => { toggleExpanding(); }; + useImperativeHandle(ref, () => ({ + scrollToItem: scrollToTargetAdjusted, + })); + return (
{!isPreviewMode && ( @@ -210,4 +222,4 @@ export const FeedCard: FC = (props) => {
); -}; +}); diff --git a/src/pages/common/components/FeedCard/types.ts b/src/pages/common/components/FeedCard/types.ts index b1d147db22..28487dd1b3 100644 --- a/src/pages/common/components/FeedCard/types.ts +++ b/src/pages/common/components/FeedCard/types.ts @@ -3,3 +3,7 @@ export interface FeedCardSettings { shouldHideCardStyles?: boolean; withHovering?: boolean; } + +export interface FeedCardRef { + scrollToItem: () => void; +} diff --git a/src/pages/common/components/FeedItem/FeedItem.tsx b/src/pages/common/components/FeedItem/FeedItem.tsx index e22d6d3c13..96b79f19fb 100644 --- a/src/pages/common/components/FeedItem/FeedItem.tsx +++ b/src/pages/common/components/FeedItem/FeedItem.tsx @@ -1,4 +1,4 @@ -import React, { FC, memo } from "react"; +import React, { forwardRef, memo } from "react"; import { FeedLayoutItemChangeData } from "@/shared/interfaces"; import { Circles, @@ -6,7 +6,6 @@ import { Common, CommonFeed, CommonFeedType, - CommonLink, CommonMember, DirectParent, } from "@/shared/models"; @@ -16,6 +15,7 @@ import { DiscussionFeedCard } from "../DiscussionFeedCard"; import { ProposalFeedCard } from "../ProposalFeedCard"; import { ProjectFeedItem } from "./components"; import { useFeedItemContext } from "./context"; +import { FeedItemRef } from "./types"; interface FeedItemProps { commonId?: string; @@ -42,7 +42,7 @@ interface FeedItemProps { directParent?: DirectParent | null; } -const FeedItem: FC = (props) => { +const FeedItem = forwardRef((props, ref) => { const { commonId, commonName, @@ -85,6 +85,7 @@ const FeedItem: FC = (props) => { }; const generalProps = { + ref, item, commonId, commonName, @@ -118,6 +119,6 @@ const FeedItem: FC = (props) => { } return null; -}; +}); export default memo(FeedItem); diff --git a/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx b/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx index e1b1ac96f0..7c948ed356 100644 --- a/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx +++ b/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx @@ -1,7 +1,6 @@ import React, { FC, ReactNode, useEffect } from "react"; import { useHistory } from "react-router-dom"; import classNames from "classnames"; -import { useCommonMember } from "@/pages/OldCommon/hooks"; import { useFeedItemContext } from "@/pages/common"; import { useRoutesContext } from "@/shared/contexts"; import { useCommon } from "@/shared/hooks/useCases"; @@ -9,6 +8,7 @@ import { OpenIcon } from "@/shared/icons"; import { CommonFeed } from "@/shared/models"; import { CommonAvatar, parseStringToTextEditorValue } from "@/shared/ui-kit"; import { checkIsProject } from "@/shared/utils"; +import { useFeedItemCounters } from "../../hooks"; import styles from "./ProjectFeedItem.module.scss"; interface ProjectFeedItemProps { @@ -22,16 +22,13 @@ export const ProjectFeedItem: FC = (props) => { const { getCommonPagePath } = useRoutesContext(); const { renderFeedItemBaseContent } = useFeedItemContext(); const { data: common, fetched: isCommonFetched, fetchCommon } = useCommon(); - const { - fetched: isCommonMemberFetched, - data: commonMember, - fetchCommonMember, - } = useCommonMember(); + const { unreadStreamsCount, unreadMessages } = useFeedItemCounters( + item.id, + common?.directParent?.commonId, + ); const commonId = item.data.id; - const unreadStreamsCount = - commonMember?.streamsUnreadCountByProjectStream[item.id] ?? null; const lastMessage = parseStringToTextEditorValue( - unreadStreamsCount !== null + Number.isInteger(unreadStreamsCount) ? `${unreadStreamsCount} updated stream${ unreadStreamsCount === 1 ? "" : "s" }` @@ -65,10 +62,6 @@ export const ProjectFeedItem: FC = (props) => { fetchCommon(commonId); }, [commonId]); - useEffect(() => { - fetchCommonMember(item.commonId); - }, [fetchCommonMember, item.commonId]); - return ( ( <> @@ -76,13 +69,12 @@ export const ProjectFeedItem: FC = (props) => { className: styles.container, titleWrapperClassName: styles.titleWrapper, lastActivity: item.updatedAt.seconds * 1000, - unreadMessages: - commonMember?.unreadCountByProjectStream[item.id] ?? 0, isMobileView: isMobileVersion, title: titleEl, onClick: handleClick, seenOnce: true, - isLoading: !isCommonFetched || !isCommonMemberFetched, + isLoading: !isCommonFetched, + unreadMessages, lastMessage, renderLeftContent, shouldHideBottomContent: !lastMessage, diff --git a/src/pages/common/components/FeedItem/hooks/index.ts b/src/pages/common/components/FeedItem/hooks/index.ts new file mode 100644 index 0000000000..269de5d6a0 --- /dev/null +++ b/src/pages/common/components/FeedItem/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useFeedItemCounters"; diff --git a/src/pages/common/components/FeedItem/hooks/useFeedItemCounters.ts b/src/pages/common/components/FeedItem/hooks/useFeedItemCounters.ts new file mode 100644 index 0000000000..3b362207db --- /dev/null +++ b/src/pages/common/components/FeedItem/hooks/useFeedItemCounters.ts @@ -0,0 +1,34 @@ +import { useEffect } from "react"; +import { useCommonMember } from "@/pages/OldCommon/hooks"; +import { useGovernanceByCommonId } from "@/shared/hooks/useCases"; + +interface Return { + unreadStreamsCount?: number; + unreadMessages?: number; +} + +export const useFeedItemCounters = ( + feedItemId: string, + commonId?: string, +): Return => { + const { data: governance, fetchGovernance } = useGovernanceByCommonId(); + const { data: commonMember } = useCommonMember({ + shouldAutoReset: false, + withSubscription: true, + governanceCircles: governance?.circles, + commonId, + }); + const { streamsUnreadCountByProjectStream, unreadCountByProjectStream } = + commonMember || {}; + + useEffect(() => { + if (commonId) { + fetchGovernance(commonId); + } + }, [fetchGovernance, commonId]); + + return { + unreadStreamsCount: streamsUnreadCountByProjectStream?.[feedItemId], + unreadMessages: unreadCountByProjectStream?.[feedItemId], + }; +}; diff --git a/src/pages/common/components/FeedItem/types.ts b/src/pages/common/components/FeedItem/types.ts index 6f76108c99..dea8ade8a1 100644 --- a/src/pages/common/components/FeedItem/types.ts +++ b/src/pages/common/components/FeedItem/types.ts @@ -28,3 +28,7 @@ export interface GetAllowedItemsOptions { } export type MenuItemOptions = Omit; + +export interface FeedItemRef { + scrollToItem: () => void; +} diff --git a/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx index 8d55b190e3..106f2bccc2 100644 --- a/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx +++ b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx @@ -1,4 +1,10 @@ -import React, { ReactNode, useCallback, useEffect, useState } from "react"; +import React, { + forwardRef, + ReactNode, + useCallback, + useEffect, + useState, +} from "react"; import { useSelector } from "react-redux"; import { selectUser } from "@/pages/Auth/store/selectors"; import { useCommonMember, useProposalUserVote } from "@/pages/OldCommon/hooks"; @@ -17,7 +23,6 @@ import { CommonFeed, Governance, PredefinedTypes, - ProposalState, ResolutionType, } from "@/shared/models"; import { TextEditorValue } from "@/shared/ui-kit"; @@ -36,7 +41,11 @@ import { FeedCountdown, FeedCardShare, } from "../FeedCard"; -import { GetLastMessageOptions, GetNonAllowedItemsOptions } from "../FeedItem"; +import { + FeedItemRef, + GetLastMessageOptions, + GetNonAllowedItemsOptions, +} from "../FeedItem"; import { ProposalFeedVotingInfo, ProposalFeedButtonContainer, @@ -73,356 +82,367 @@ interface ProposalFeedCardProps { onUserSelect?: (userId: string, commonId?: string) => void; } -const ProposalFeedCard: React.FC = (props) => { - const { - commonId, - commonName, - commonImage, - pinnedFeedItems, - isProject, - isPinned, - item, - governanceCircles, - isPreviewMode, - isActive, - isExpanded, - getLastMessage, - getNonAllowedItems, - isMobileVersion, - onActiveItemDataChange, - onUserSelect, - } = props; - const user = useSelector(selectUser()); - const userId = user?.uid; - const { setChatItem, feedItemIdForAutoChatOpen, shouldAllowChatAutoOpen } = - useChatContext(); - const forceUpdate = useForceUpdate(); - const { getCommonPagePath } = useRoutesContext(); - const { - fetchUser: fetchFeedItemUser, - data: feedItemUser, - fetched: isFeedItemUserFetched, - } = useUserById(); - const { - fetchDiscussion, - data: discussion, - fetched: isDiscussionFetched, - } = useDiscussionById(); - const { - fetchProposal, - data: proposal, - fetched: isProposalFetched, - } = useProposalById(); - const { - fetched: isCommonMemberFetched, - data: commonMember, - fetchCommonMember, - } = useCommonMember(); - const { - data: userVote, - loading: isUserVoteLoading, - fetchProposalVote, - setVote, - } = useProposalUserVote(); - const { - data: proposalSpecificData, - fetched: isProposalSpecificDataFetched, - fetchData: fetchProposalSpecificData, - } = useProposalSpecificData(); - const { - data: feedItemUserMetadata, - fetched: isFeedItemUserMetadataFetched, - fetchFeedItemUserMetadata, - } = useFeedItemUserMetadata(); - const isLoading = - !isFeedItemUserFetched || - !isDiscussionFetched || - !isProposalFetched || - !proposal || - isUserVoteLoading || - !isCommonMemberFetched || - !isProposalSpecificDataFetched || - !isFeedItemUserMetadataFetched || - !commonId || - !governanceCircles; - const [isHovering, setHovering] = useState(false); - const onHover = (isMouseEnter: boolean): void => { - setHovering(isMouseEnter); - }; - const proposalId = item.data.id; - const { - isShowing: isShareModalOpen, - onOpen: onShareModalOpen, - onClose: onShareModalClose, - } = useModal(false); - const feedItemFollow = useFeedItemFollow(item.id, commonId); - const menuItems = useMenuItems( - { +const ProposalFeedCard = forwardRef( + (props, ref) => { + const { commonId, + commonName, + commonImage, pinnedFeedItems, - feedItem: item, - discussion, + isProject, + isPinned, + item, governanceCircles, - commonMember, - feedItemFollow, + isPreviewMode, + isActive, + isExpanded, + getLastMessage, getNonAllowedItems, - }, - { - report: () => {}, - share: () => onShareModalOpen(), - }, - ); - const cardTitle = discussion?.title; + isMobileVersion, + onActiveItemDataChange, + onUserSelect, + } = props; + const user = useSelector(selectUser()); + const userId = user?.uid; + const { setChatItem, feedItemIdForAutoChatOpen, shouldAllowChatAutoOpen } = + useChatContext(); + const forceUpdate = useForceUpdate(); + const { getCommonPagePath } = useRoutesContext(); + const { + fetchUser: fetchFeedItemUser, + data: feedItemUser, + fetched: isFeedItemUserFetched, + } = useUserById(); + const { + fetchDiscussion, + data: discussion, + fetched: isDiscussionFetched, + } = useDiscussionById(); + const { + fetchProposal, + data: proposal, + fetched: isProposalFetched, + } = useProposalById(); + const { + fetched: isCommonMemberFetched, + data: commonMember, + fetchCommonMember, + } = useCommonMember(); + const { + data: userVote, + loading: isUserVoteLoading, + fetchProposalVote, + setVote, + } = useProposalUserVote(); + const { + data: proposalSpecificData, + fetched: isProposalSpecificDataFetched, + fetchData: fetchProposalSpecificData, + } = useProposalSpecificData(); + const { + data: feedItemUserMetadata, + fetched: isFeedItemUserMetadataFetched, + fetchFeedItemUserMetadata, + } = useFeedItemUserMetadata(); + const isLoading = + !isFeedItemUserFetched || + !isDiscussionFetched || + !isProposalFetched || + !proposal || + isUserVoteLoading || + !isCommonMemberFetched || + !isProposalSpecificDataFetched || + !isFeedItemUserMetadataFetched || + !commonId || + !governanceCircles; + const [isHovering, setHovering] = useState(false); + const onHover = (isMouseEnter: boolean): void => { + setHovering(isMouseEnter); + }; + const proposalId = item.data.id; + const { + isShowing: isShareModalOpen, + onOpen: onShareModalOpen, + onClose: onShareModalClose, + } = useModal(false); + const feedItemFollow = useFeedItemFollow(item.id, commonId); + const menuItems = useMenuItems( + { + commonId, + pinnedFeedItems, + feedItem: item, + discussion, + governanceCircles, + commonMember, + feedItemFollow, + getNonAllowedItems, + }, + { + report: () => {}, + share: () => onShareModalOpen(), + }, + ); + const cardTitle = discussion?.title; - useEffect(() => { - fetchFeedItemUser(item.userId); - }, [item.userId]); + useEffect(() => { + fetchFeedItemUser(item.userId); + }, [item.userId]); - useEffect(() => { - if (item.data.discussionId) { - fetchDiscussion(item.data.discussionId); - } - }, [item.data.discussionId]); + useEffect(() => { + if (item.data.discussionId) { + fetchDiscussion(item.data.discussionId); + } + }, [item.data.discussionId]); - useEffect(() => { - fetchProposal(item.data.id); - }, [item.data.id]); + useEffect(() => { + fetchProposal(item.data.id); + }, [item.data.id]); - useEffect(() => { - fetchProposalVote(proposalId); - }, [fetchProposalVote, proposalId]); + useEffect(() => { + fetchProposalVote(proposalId); + }, [fetchProposalVote, proposalId]); - useEffect(() => { - if (commonId) { - fetchCommonMember(commonId, {}); - } - }, [fetchCommonMember, commonId]); + useEffect(() => { + if (commonId) { + fetchCommonMember(commonId, {}); + } + }, [fetchCommonMember, commonId]); - useEffect(() => { - if (commonId) { - fetchFeedItemUserMetadata({ - userId: userId || "", - commonId, - feedObjectId: item.id, - }); - } - }, [userId, commonId, item.id]); + useEffect(() => { + if (commonId) { + fetchFeedItemUserMetadata({ + userId: userId || "", + commonId, + feedObjectId: item.id, + }); + } + }, [userId, commonId, item.id]); - useEffect(() => { - if (proposal) { - fetchProposalSpecificData(proposal, true); - } - }, [proposal?.id]); + useEffect(() => { + if (proposal) { + fetchProposalSpecificData(proposal, true); + } + }, [proposal?.id]); - useEffect(() => { - if (isActive && cardTitle) { - onActiveItemDataChange?.({ - itemId: item.id, - title: cardTitle, - }); - } - }, [isActive, cardTitle]); + useEffect(() => { + if (isActive && cardTitle) { + onActiveItemDataChange?.({ + itemId: item.id, + title: cardTitle, + }); + } + }, [isActive, cardTitle]); - const handleOpenChat = useCallback(() => { - if (discussion && proposal) { - setChatItem({ - feedItemId: item.id, - discussion, - proposal, - circleVisibility: item.circleVisibility, - lastSeenItem: feedItemUserMetadata?.lastSeen, - lastSeenAt: feedItemUserMetadata?.lastSeenAt, - seenOnce: feedItemUserMetadata?.seenOnce, - }); - } - }, [ - item.id, - proposal, - discussion, - setChatItem, - item.circleVisibility, - feedItemUserMetadata?.lastSeen, - feedItemUserMetadata?.lastSeenAt, - feedItemUserMetadata?.seenOnce, - ]); + const handleOpenChat = useCallback(() => { + if (discussion && proposal) { + setChatItem({ + feedItemId: item.id, + discussion, + proposal, + circleVisibility: item.circleVisibility, + lastSeenItem: feedItemUserMetadata?.lastSeen, + lastSeenAt: feedItemUserMetadata?.lastSeenAt, + seenOnce: feedItemUserMetadata?.seenOnce, + }); + } + }, [ + item.id, + proposal, + discussion, + setChatItem, + item.circleVisibility, + feedItemUserMetadata?.lastSeen, + feedItemUserMetadata?.lastSeenAt, + feedItemUserMetadata?.seenOnce, + ]); - useEffect(() => { - if ( - isDiscussionFetched && - isProposalFetched && - isFeedItemUserMetadataFetched && - item.id === feedItemIdForAutoChatOpen && - !isMobileVersion && - shouldAllowChatAutoOpen !== false - ) { - handleOpenChat(); - } - }, [ - isDiscussionFetched, - isProposalFetched, - isFeedItemUserMetadataFetched, - shouldAllowChatAutoOpen, - ]); + useEffect(() => { + if ( + (!isActive || + shouldAllowChatAutoOpen === null || + shouldAllowChatAutoOpen) && + isDiscussionFetched && + isProposalFetched && + isFeedItemUserMetadataFetched && + item.id === feedItemIdForAutoChatOpen && + !isMobileVersion + ) { + handleOpenChat(); + } + }, [ + isDiscussionFetched, + isProposalFetched, + isFeedItemUserMetadataFetched, + shouldAllowChatAutoOpen, + ]); - useEffect(() => { - if (isExpanded) { - forceUpdate(); - } - }, [isExpanded]); + useEffect(() => { + if (isActive && shouldAllowChatAutoOpen !== null) { + handleOpenChat(); + } + }, [isActive, shouldAllowChatAutoOpen, handleOpenChat]); - const renderContent = (): ReactNode => { - if (isLoading) { - return null; - } + useEffect(() => { + if (isExpanded) { + forceUpdate(); + } + }, [isExpanded]); - const isCountdownState = checkIsCountdownState(proposal); - const userHasPermissionsToVote = checkUserPermissionsToVote({ - proposal, - commonMember, - }); - const isVotingAllowed = - userHasPermissionsToVote && - checkIsVotingAllowed({ - userVote, + const renderContent = (): ReactNode => { + if (isLoading) { + return null; + } + + const isCountdownState = checkIsCountdownState(proposal); + const userHasPermissionsToVote = checkUserPermissionsToVote({ proposal, + commonMember, }); - const circleVisibility = getVisibilityString( - governanceCircles, - item.circleVisibility, - proposal?.type, - getUserName(feedItemUser), - ); + const isVotingAllowed = + userHasPermissionsToVote && + checkIsVotingAllowed({ + userVote, + proposal, + }); + const circleVisibility = getVisibilityString( + governanceCircles, + item.circleVisibility, + proposal?.type, + getUserName(feedItemUser), + ); + + return ( + <> + + Created:{" "} + + + } + type={getProposalTypeString(proposal.type)} + circleVisibility={circleVisibility} + commonId={commonId} + userId={item.userId} + menuItems={menuItems} + onUserSelect={ + onUserSelect && (() => onUserSelect(item.userId, commonId)) + } + /> + { + onHover(true); + }} + onMouseLeave={() => { + onHover(false); + }} + > + {proposal.resolutionType === ResolutionType.WAIT_FOR_EXPIRATION && ( + <> + + + + )} + + {proposal.resolutionType === ResolutionType.IMMEDIATE && ( + <> + + + + )} + + {isVotingAllowed && ( + + )} + + + ); + }; return ( <> - - Created:{" "} - - - } - type={getProposalTypeString(proposal.type)} - circleVisibility={circleVisibility} - commonId={commonId} - userId={item.userId} - menuItems={menuItems} - onUserSelect={ - onUserSelect && (() => onUserSelect(item.userId, commonId)) - } - /> - { - onHover(true); - }} - onMouseLeave={() => { - onHover(false); - }} + lastActivity={item.updatedAt.seconds * 1000} + isActive={isActive} + isExpanded={isExpanded} + unreadMessages={feedItemUserMetadata?.count || 0} + title={cardTitle} + lastMessage={getLastMessage({ + commonFeedType: item.data.type, + lastMessage: item.data.lastMessage, + discussion, + currentUserId: userId, + feedItemCreatorName: getUserName(feedItemUser), + commonName, + isProject, + hasFiles: item.data.hasFiles, + hasImages: item.data.hasImages, + })} + canBeExpanded={discussion?.predefinedType !== PredefinedTypes.General} + isPreviewMode={isPreviewMode} + image={commonImage} + imageAlt={`${commonName}'s image`} + isProject={isProject} + isPinned={isPinned} + isFollowing={feedItemFollow.isFollowing} + isLoading={isLoading} + type={item.data.type} + seenOnce={feedItemUserMetadata?.seenOnce} + menuItems={menuItems} + ownerId={item.userId} > - {proposal.resolutionType === ResolutionType.WAIT_FOR_EXPIRATION && ( - <> - - - - )} - - {proposal.resolutionType === ResolutionType.IMMEDIATE && ( - <> - - - - )} - - {isVotingAllowed && ( - - )} - + {renderContent()} + + {discussion && ( + + )} ); - }; - - return ( - <> - - {renderContent()} - - {discussion && ( - - )} - - ); -}; + }, +); export default ProposalFeedCard; diff --git a/src/pages/common/providers/CommonData/CommonData.tsx b/src/pages/common/providers/CommonData/CommonData.tsx index 3a7b44fb62..5416e87d4a 100644 --- a/src/pages/common/providers/CommonData/CommonData.tsx +++ b/src/pages/common/providers/CommonData/CommonData.tsx @@ -318,11 +318,8 @@ const CommonData: FC = (props) => { onClose={handleCommonJoinModalClose} common={common} governance={governance} - shouldShowLoadingAfterSuccessfulCreation={ - governance.proposals[ProposalsTypes.MEMBER_ADMITTANCE]?.global - .votingDuration === 0 - } onRequestCreated={handleCommonJoinRequestCreated} + showLoadingAfterSuccessfulCreation /> = (props) => { fetchUserRelatedData(); }; - const fetchMoreCommonFeedItems = () => { + const fetchMoreCommonFeedItems = (feedItemId?: string) => { if (hasMoreCommonFeedItems) { - fetchCommonFeedItems(); + fetchCommonFeedItems(feedItemId); } }; diff --git a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx index 85616f1b7f..dc7cbb6ef0 100644 --- a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx +++ b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx @@ -7,6 +7,7 @@ import React, { useEffect, useImperativeHandle, useMemo, + useRef, useState, } from "react"; import { useDispatch, useSelector } from "react-redux"; @@ -21,6 +22,7 @@ import { FeedItemBaseContentProps, FeedItemContext, FeedItemContextValue, + FeedItemRef, GetLastMessageOptions, GetNonAllowedItemsOptions, } from "@/pages/common"; @@ -31,7 +33,9 @@ import { import { ChatContext } from "@/pages/common/components/ChatComponent/context"; import { JoinProjectModal } from "@/pages/common/components/JoinProjectModal"; import { useJoinProjectAutomatically } from "@/pages/common/hooks"; -import { InboxItemType, QueryParamKey } from "@/shared/constants"; +import { InternalLinkData } from "@/shared/components"; +import { InboxItemType, QueryParamKey, ROUTE_PATHS } from "@/shared/constants"; +import { useRoutesContext } from "@/shared/contexts"; import { useAuthorizedModal, useQueryParams } from "@/shared/hooks"; import { useGovernanceByCommonId } from "@/shared/hooks/useCases"; import { useIsTabletView } from "@/shared/hooks/viewport"; @@ -59,6 +63,7 @@ import { addQueryParam, checkIsProject, deleteQueryParam, + getParamsFromOneOfRoutes, getUserName, } from "@/shared/utils"; import { commonActions, selectRecentStreamId } from "@/store/states"; @@ -110,7 +115,7 @@ interface FeedLayoutProps { topFeedItems?: FeedLayoutItem[]; loading: boolean; shouldHideContent?: boolean; - onFetchNext: () => void; + onFetchNext: (feedItemId?: string) => void; renderFeedItemBaseContent: (props: FeedItemBaseContentProps) => ReactNode; renderChatChannelItem?: (props: ChatChannelFeedLayoutItemProps) => ReactNode; onFeedItemUpdate?: (item: CommonFeed, isRemoved: boolean) => void; @@ -124,6 +129,11 @@ interface FeedLayoutProps { feedItem: FeedLayoutItem, becameEmpty: boolean, ) => void; + onFeedItemSelect?: ( + commonId: string, + feedItemId: string, + messageId?: string, + ) => void; outerStyles?: FeedLayoutOuterStyles; settings?: FeedLayoutSettings; } @@ -155,10 +165,13 @@ const FeedLayout: ForwardRefRenderFunction = ( onActiveItemChange, onActiveItemDataChange, onMessagesAmountEmptinessToggle, + onFeedItemSelect, outerStyles, settings, } = props; const dispatch = useDispatch(); + const { getCommonPagePath } = useRoutesContext(); + const refsByItemId = useRef>({}); const { width: windowWidth } = useWindowSize(); const history = useHistory(); const queryParams = useQueryParams(); @@ -463,6 +476,102 @@ const FeedLayout: ForwardRefRenderFunction = ( ? handleDMClick : undefined; + const handleFeedItemClickExternal = useCallback( + ( + feedItemId: string, + options: { commonId?: string; messageId?: string } = {}, + ) => { + const { commonId = selectedItemCommonData?.id, messageId } = options; + + if (commonId) { + onFeedItemSelect?.(commonId, feedItemId, messageId); + } + }, + [selectedItemCommonData?.id, onFeedItemSelect], + ); + + const handleFeedItemClickInternal = ( + feedItemId: string, + options: { commonId?: string; messageId?: string } = {}, + ) => { + const { commonId, messageId } = options; + + if (commonId && commonId !== outerCommon?.id) { + history.push( + getCommonPagePath(commonId, { + item: feedItemId, + message: messageId, + }), + ); + return; + } + + setActiveChatItem({ + feedItemId, + circleVisibility: [], + }); + + const itemExists = allFeedItems.some((item) => item.itemId === feedItemId); + + if (itemExists) { + refsByItemId.current[feedItemId]?.scrollToItem(); + } else { + onFetchNext(feedItemId); + setTimeout(() => { + window.scrollTo({ + top: document.body.scrollHeight, + behavior: "smooth", + }); + }, 50); + } + + if (messageId) { + addQueryParam(QueryParamKey.Message, messageId); + } + }; + + const handleFeedItemClick = onFeedItemSelect + ? handleFeedItemClickExternal + : handleFeedItemClickInternal; + + const handleInternalLinkClick = useCallback( + (data: InternalLinkData) => { + const feedPageParams = getParamsFromOneOfRoutes<{ id: string }>( + data.pathname, + [ROUTE_PATHS.COMMON, ROUTE_PATHS.V04_COMMON], + ); + + if (!feedPageParams) { + return; + } + + const itemId = data.params[QueryParamKey.Item]; + const messageId = data.params[QueryParamKey.Message]; + + if (itemId) { + handleFeedItemClick(itemId, { + commonId: feedPageParams.id, + messageId, + }); + return; + } + + history.push( + getCommonPagePath(feedPageParams.id, { + item: itemId, + message: messageId, + }), + ); + }, + [getCommonPagePath, handleFeedItemClick], + ); + + useEffect(() => { + if (commonMember && isCommonJoinModalOpen) { + onCommonJoinModalClose(); + } + }, [commonMember?.id]); + useEffect(() => { if (!outerGovernance && selectedItemCommonData?.id) { fetchGovernance(selectedItemCommonData.id); @@ -490,7 +599,10 @@ const FeedLayout: ForwardRefRenderFunction = ( }, [activeFeedItemId]); useEffect(() => { - if (selectedFeedItem?.itemId) { + if (selectedFeedItem?.itemId && !isTabletView) { + refsByItemId.current[selectedFeedItem.itemId]?.scrollToItem(); + } + if (selectedFeedItem?.itemId || (chatItem && !chatItem.discussion)) { return; } @@ -585,6 +697,9 @@ const FeedLayout: ForwardRefRenderFunction = ( return ( { + refsByItemId.current[item.itemId] = ref; + }} key={item.feedItem.id} commonMember={commonMember} commonId={commonData?.id} @@ -628,7 +743,7 @@ const FeedLayout: ForwardRefRenderFunction = ( })} {!isTabletView && - (chatItem ? ( + (chatItem?.discussion ? ( = ( onJoinCommon={onJoinCommon} isJoinPending={isJoinPending} onUserClick={handleUserClick} + onFeedItemClick={handleFeedItemClick} + onInternalLinkClick={handleInternalLinkClick} /> ) : ( ))} @@ -666,6 +783,8 @@ const FeedLayout: ForwardRefRenderFunction = ( onJoinCommon={onJoinCommon} isJoinPending={isJoinPending} onUserClick={handleUserClick} + onFeedItemClick={handleFeedItemClick} + onInternalLinkClick={handleInternalLinkClick} > {selectedItemCommonData && checkIsFeedItemFollowLayoutItem(selectedFeedItem) && ( @@ -692,6 +811,7 @@ const FeedLayout: ForwardRefRenderFunction = ( onClose={onCommonJoinModalClose} common={outerCommon} governance={governance} + showLoadingAfterSuccessfulCreation /> void; onUserClick?: (userId: string) => void; + onFeedItemClick?: (feedItemId: string) => void; + onInternalLinkClick?: (data: InternalLinkData) => void; } const DesktopChat: FC = (props) => { @@ -49,6 +51,8 @@ const DesktopChat: FC = (props) => { isJoinPending, onJoinCommon, onUserClick, + onFeedItemClick, + onInternalLinkClick, } = props; const { fetchUser: fetchDMUser, @@ -64,7 +68,7 @@ const DesktopChat: FC = (props) => { const dmUserId = chatItem.chatChannel?.participants.filter( (participant) => participant !== userId, )[0]; - const title = getUserName(dmUser) || chatItem.discussion.title; + const title = getUserName(dmUser) || chatItem.discussion?.title || ""; const hasAccessToChat = useMemo( () => checkHasAccessToChat(userCircleIds, chatItem), @@ -122,6 +126,8 @@ const DesktopChat: FC = (props) => { isJoinPending={isJoinPending} onJoinCommon={onJoinCommon} onUserClick={onUserClick} + onFeedItemClick={onFeedItemClick} + onInternalLinkClick={onInternalLinkClick} /> ); diff --git a/src/pages/commonFeed/components/FeedLayout/components/MobileChat/MobileChat.tsx b/src/pages/commonFeed/components/FeedLayout/components/MobileChat/MobileChat.tsx index 6149f16fc9..c06977558f 100644 --- a/src/pages/commonFeed/components/FeedLayout/components/MobileChat/MobileChat.tsx +++ b/src/pages/commonFeed/components/FeedLayout/components/MobileChat/MobileChat.tsx @@ -8,6 +8,7 @@ import { useChatContext, } from "@/pages/common/components/ChatComponent"; import { checkHasAccessToChat } from "@/pages/common/components/CommonTabPanels/components"; +import { InternalLinkData } from "@/shared/components"; import { ChatType } from "@/shared/constants"; import { useUserById } from "@/shared/hooks/useCases"; import { @@ -36,6 +37,8 @@ interface ChatProps { onClose: () => void; onJoinCommon?: () => void; onUserClick?: (userId: string) => void; + onFeedItemClick?: (feedItemId: string) => void; + onInternalLinkClick?: (data: InternalLinkData) => void; } const MobileChat: FC = (props) => { @@ -55,6 +58,8 @@ const MobileChat: FC = (props) => { onJoinCommon, onClose, onUserClick, + onFeedItemClick, + onInternalLinkClick, } = props; const { setIsShowFeedItemDetailsModal } = useChatContext(); const { @@ -73,9 +78,9 @@ const MobileChat: FC = (props) => { )[0]; const title = getUserName(dmUser) || - (chatItem?.discussion.predefinedType === PredefinedTypes.General + (chatItem?.discussion?.predefinedType === PredefinedTypes.General ? commonName - : chatItem?.discussion.title || ""); + : chatItem?.discussion?.title || ""); const hasAccessToChat = useMemo( () => checkHasAccessToChat(userCircleIds, chatItem), @@ -152,6 +157,8 @@ const MobileChat: FC = (props) => { isJoinPending={isJoinPending} onJoinCommon={onJoinCommon} onUserClick={onUserClick} + onFeedItemClick={onFeedItemClick} + onInternalLinkClick={onInternalLinkClick} /> )} diff --git a/src/pages/commonFeed/components/FeedLayout/utils/checkShouldAutoOpenPreview.ts b/src/pages/commonFeed/components/FeedLayout/utils/checkShouldAutoOpenPreview.ts index 91619c9ed6..71fc5c2de6 100644 --- a/src/pages/commonFeed/components/FeedLayout/utils/checkShouldAutoOpenPreview.ts +++ b/src/pages/commonFeed/components/FeedLayout/utils/checkShouldAutoOpenPreview.ts @@ -5,10 +5,16 @@ import { checkIsCountdownState } from "@/shared/utils"; export const checkShouldAutoOpenPreview = ( chatItem?: ChatItem | null, ): boolean => { - if (!chatItem) { + if (!chatItem || !chatItem.discussion) { return false; } - if (!chatItem.seenOnce || chatItem.proposal?.state === ProposalState.VOTING) { + if ( + chatItem.proposal && + (!chatItem.seenOnce || chatItem.proposal.state === ProposalState.VOTING) + ) { + return true; + } + if (chatItem.discussion && !chatItem.seenOnce) { return true; } const expirationTimestamp = diff --git a/src/pages/inbox/BaseInbox.tsx b/src/pages/inbox/BaseInbox.tsx index 6da8afd5fa..93221eaa16 100644 --- a/src/pages/inbox/BaseInbox.tsx +++ b/src/pages/inbox/BaseInbox.tsx @@ -8,6 +8,7 @@ import React, { useState, } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { useHistory } from "react-router-dom"; import { selectUser } from "@/pages/Auth/store/selectors"; import { FeedItemBaseContentProps } from "@/pages/common"; import { @@ -16,6 +17,7 @@ import { FeedLayoutSettings, } from "@/pages/commonFeed"; import { QueryParamKey } from "@/shared/constants"; +import { useRoutesContext } from "@/shared/contexts"; import { ChatChannelToDiscussionConverter } from "@/shared/converters"; import { useQueryParams } from "@/shared/hooks"; import { useInboxItems } from "@/shared/hooks/useCases"; @@ -60,6 +62,8 @@ const InboxPage: FC = (props) => { } = props; const queryParams = useQueryParams(); const dispatch = useDispatch(); + const history = useHistory(); + const { getCommonPagePath } = useRoutesContext(); const [feedLayoutRef, setFeedLayoutRef] = useState( null, ); @@ -156,6 +160,18 @@ const InboxPage: FC = (props) => { [dispatch], ); + const handleFeedItemSelect = useCallback( + (commonId: string, feedItemId: string, messageId?: string) => { + history.push( + getCommonPagePath(commonId, { + item: feedItemId, + message: messageId, + }), + ); + }, + [history.push, getCommonPagePath], + ); + useEffect(() => { dispatch(inboxActions.setSharedFeedItemId(sharedFeedItemId)); @@ -250,6 +266,7 @@ const InboxPage: FC = (props) => { onActiveItemChange={handleActiveItemChange} onActiveItemDataChange={onActiveItemDataChange} onMessagesAmountEmptinessToggle={handleMessagesAmountEmptinessToggle} + onFeedItemSelect={handleFeedItemSelect} outerStyles={feedLayoutOuterStyles} settings={feedLayoutSettings} /> diff --git a/src/pages/inbox/utils/getNonAllowedItems.ts b/src/pages/inbox/utils/getNonAllowedItems.ts index 0dcba5f370..8fb89fd88f 100644 --- a/src/pages/inbox/utils/getNonAllowedItems.ts +++ b/src/pages/inbox/utils/getNonAllowedItems.ts @@ -1,15 +1,8 @@ import { FeedItemMenuItem, GetNonAllowedItemsOptions } from "@/pages/common"; -import { CommonFeedType } from "@/shared/models"; -export const getNonAllowedItems: GetNonAllowedItemsOptions = (type) => { - const items: FeedItemMenuItem[] = [ - FeedItemMenuItem.Pin, - FeedItemMenuItem.Unpin, - ]; - - if (type !== CommonFeedType.Discussion) { - items.push(FeedItemMenuItem.Remove); - } - - return items; -}; +export const getNonAllowedItems: GetNonAllowedItemsOptions = () => [ + FeedItemMenuItem.Pin, + FeedItemMenuItem.Unpin, + FeedItemMenuItem.Edit, + FeedItemMenuItem.Remove, +]; diff --git a/src/services/CommonFeed.ts b/src/services/CommonFeed.ts index 37fbcf7848..c4269783b0 100644 --- a/src/services/CommonFeed.ts +++ b/src/services/CommonFeed.ts @@ -78,6 +78,7 @@ class CommonFeedService { commonId: string, options: { startAfter?: Timestamp | null; + feedItemId?: string; limit?: number; } = {}, ): Promise<{ @@ -86,12 +87,13 @@ class CommonFeedService { lastDocTimestamp: Timestamp | null; hasMore: boolean; }> => { - const { startAfter, limit = 10 } = options; + const { startAfter, feedItemId, limit = 10 } = options; const endpoint = ApiEndpoint.GetCommonFeedItems.replace( ":commonId", commonId, ); const queryParams: Record = { + feedItemId, limit, }; diff --git a/src/shared/components/Chat/ChatMessage/ChatMessage.module.scss b/src/shared/components/Chat/ChatMessage/ChatMessage.module.scss index 6a6f612c2b..ee1bf0d5cf 100644 --- a/src/shared/components/Chat/ChatMessage/ChatMessage.module.scss +++ b/src/shared/components/Chat/ChatMessage/ChatMessage.module.scss @@ -108,6 +108,7 @@ white-space: pre-line; margin-right: 1rem; margin-left: 1rem; + word-break: break-word; } .messageContentCurrentUser { @@ -128,6 +129,7 @@ .systemMessageCommonLink { color: $c-pink-mention; text-decoration: none; + cursor: pointer; &:hover { text-decoration: underline; diff --git a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx index 0bbd90d800..7cf8c2cd0e 100644 --- a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx +++ b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx @@ -8,12 +8,16 @@ import React, { } from "react"; import classNames from "classnames"; import { - Linkify, ElementDropdown, UserAvatar, UserInfoPopup, } from "@/shared/components"; -import { Orientation, ChatType, EntityTypes } from "@/shared/constants"; +import { + Orientation, + ChatType, + EntityTypes, + QueryParamKey, +} from "@/shared/constants"; import { Colors } from "@/shared/constants"; import { useRoutesContext } from "@/shared/contexts"; import { useModal } from "@/shared/hooks"; @@ -40,7 +44,7 @@ import { StaticLinkType, isRTL } from "@/shared/utils"; import { getUserName } from "@/shared/utils"; import { convertBytes } from "@/shared/utils/convertBytes"; import { EditMessageInput } from "../EditMessageInput"; -import { Time } from "./components/Time"; +import { ChatMessageLinkify, InternalLinkData, Time } from "./components"; import { getTextFromTextEditorString } from "./utils"; import styles from "./ChatMessage.module.scss"; @@ -63,6 +67,8 @@ interface ChatMessageProps { onMessageDelete?: (messageId: string) => void; directParent?: DirectParent | null; onUserClick?: (userId: string) => void; + onFeedItemClick?: (feedItemId: string) => void; + onInternalLinkClick?: (data: InternalLinkData) => void; } const getStaticLinkByChatType = (chatType: ChatType): StaticLinkType => { @@ -92,6 +98,8 @@ export default function ChatMessage({ onMessageDelete, directParent, onUserClick, + onFeedItemClick, + onInternalLinkClick, }: ChatMessageProps) { const messageRef = useRef(null); const { getCommonPagePath, getCommonPageAboutTabPath } = useRoutesContext(); @@ -168,6 +176,7 @@ export default function ChatMessage({ getCommonPageAboutTabPath, directParent, onUserClick, + onFeedItemClick, }); setMessageText(parsedText); @@ -195,6 +204,7 @@ export default function ChatMessage({ commonId: discussionMessage.commonId, directParent, onUserClick, + onFeedItemClick, }); setReplyMessageText(parsedText); @@ -228,6 +238,22 @@ export default function ChatMessage({ } }; + const handleInternalLinkClick = useCallback( + (data: InternalLinkData) => { + const messageId = data.params[QueryParamKey.Message]; + + if ( + data.params[QueryParamKey.Item] === feedItemId && + typeof messageId === "string" + ) { + scrollToRepliedMessage(messageId); + } else { + onInternalLinkClick?.(data); + } + }, + [feedItemId, scrollToRepliedMessage, onInternalLinkClick], + ); + const ReplyMessage = useCallback(() => { if ( !discussionMessage.parentMessage?.id || @@ -295,7 +321,9 @@ export default function ChatMessage({ )} ) : ( - {replyMessageText.map((text) => text)} + + {replyMessageText.map((text) => text)} + )} @@ -385,7 +413,11 @@ export default function ChatMessage({ /> )} - {messageText.map((text) => text)} + + {messageText.map((text) => text)} + {!isSystemMessage && (