diff --git a/package.json b/package.json index bd0a4db274..1553bfedca 100644 --- a/package.json +++ b/package.json @@ -175,6 +175,7 @@ "identity-obj-proxy": "^3.0.0", "jest": "^27.5.1", "jest-extended": "^2.0.0", + "jest-fetch-mock": "^3.0.3", "lint-staged": ">=10", "prettier": "^2.1.1", "prop-types": "^15.8.1", diff --git a/src/pages/OldCommon/components/CommonDetailContainer/MembersComponent/CommonMemberComponent.tsx b/src/pages/OldCommon/components/CommonDetailContainer/MembersComponent/CommonMemberComponent.tsx index 95d73d6fc9..913a2b3e48 100644 --- a/src/pages/OldCommon/components/CommonDetailContainer/MembersComponent/CommonMemberComponent.tsx +++ b/src/pages/OldCommon/components/CommonDetailContainer/MembersComponent/CommonMemberComponent.tsx @@ -122,11 +122,13 @@ const CommonMember: FC = ({ -
- {joinedAt - .toDate() - .toLocaleDateString("en-US", { month: "short", day: "numeric" })} -
+ {joinedAt && joinedAt.toDate && ( +
+ {joinedAt + ?.toDate() + .toLocaleDateString("en-US", { month: "short", day: "numeric" })} +
+ )} = ({ common }) => { () => [...commonMembers].sort( (commonMember, prevCommonMember) => - prevCommonMember.joinedAt.seconds - commonMember.joinedAt.seconds, + prevCommonMember.joinedAt?.seconds - commonMember.joinedAt?.seconds, ), [commonMembers], ); diff --git a/src/pages/OldCommon/hooks/useCommonMember.ts b/src/pages/OldCommon/hooks/useCommonMember.ts index f749b2b2e1..8d01168e8d 100644 --- a/src/pages/OldCommon/hooks/useCommonMember.ts +++ b/src/pages/OldCommon/hooks/useCommonMember.ts @@ -141,6 +141,7 @@ export const useCommonMember = (options: Options = {}): Return => { loading: false, fetched: true, data: { + commonId, ...commonMember, ...generateCirclesDataForCommonMember( governance.circles, @@ -189,7 +190,7 @@ export const useCommonMember = (options: Options = {}): Return => { } } }, - [state, userId], + [state, userId, commonId], ); const setCommonMember = useCallback( diff --git a/src/pages/OldCommon/hooks/useCommonMembers.ts b/src/pages/OldCommon/hooks/useCommonMembers.ts index 34aa60f0ea..f07c8221f5 100644 --- a/src/pages/OldCommon/hooks/useCommonMembers.ts +++ b/src/pages/OldCommon/hooks/useCommonMembers.ts @@ -1,5 +1,6 @@ import { useCallback, useRef, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { trace } from "firebase/performance"; import { CommonService, Logger, UserService } from "@/services"; import { store } from "@/shared/appConfig"; import { LoadingState } from "@/shared/interfaces"; @@ -10,6 +11,7 @@ import { selectUserStates, } from "@/store/states"; import { useDeepCompareEffect } from "react-use"; +import { perf } from "@/shared/utils/firebase"; interface Options { commonId?: string; @@ -111,6 +113,9 @@ export const useCommonMembers = ({ commonId }: Options): Return => { (async () => { try { + const useCommonMembersTrace = trace(perf, 'useCommonMembers'); + useCommonMembersTrace.start(); + const cachedUserStates = selectUserStates()(store.getState()); const hasUsersFromCache = commonMembers.some( ({ userId }) => cachedUserStates[userId]?.data, @@ -136,7 +141,7 @@ export const useCommonMembers = ({ commonId }: Options): Return => { const user = cachedUserStates[commonMember.userId]?.data; - return user ? [...acc, { ...commonMember, user }] : acc; + return user ? [...acc, { ...commonMember, user, commonId }] : acc; }, []); return { @@ -173,7 +178,7 @@ export const useCommonMembers = ({ commonId }: Options): Return => { ({ uid }) => uid === commonMember.userId, ); - return user ? [...acc, { ...commonMember, user }] : acc; + return user ? [...acc, { ...commonMember, user, commonId }] : acc; }, []); return { @@ -183,6 +188,7 @@ export const useCommonMembers = ({ commonId }: Options): Return => { }; }); dispatch(cacheActions.updateUserStates(fetchedUsers)); + useCommonMembersTrace.stop(); } catch (err) { Logger.error(err); setState((prevState) => ({ @@ -192,7 +198,7 @@ export const useCommonMembers = ({ commonId }: Options): Return => { })); } })(); - }, [commonMembersState.data]); + }, [commonMembersState.data, commonId]); return { ...state, diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 096516ceaa..de1e7796fd 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -206,10 +206,15 @@ export default function ChatComponent({ chatChannelId: chatChannel?.id || "", participants: chatChannel?.participants, }); - const users = chatChannel ? chatUsers : discussionUsers; - const discussionMessages = chatChannel - ? chatMessagesData.data - : discussionMessagesData.data || []; + const users = useMemo( + () => (chatChannel ? chatUsers : discussionUsers), + [chatUsers, discussionUsers, chatChannel], + ); + const discussionMessages = useMemo( + () => + chatChannel ? chatMessagesData.data : discussionMessagesData.data || [], + [chatChannel, chatMessagesData.data, discussionMessagesData.data], + ); const isFetchedDiscussionMessages = discussionMessagesData.fetched || chatMessagesData.fetched; const areInitialMessagesLoading = isChatChannel diff --git a/src/pages/common/components/CommonTabPanels/components/MembersTab/components/Members/Members.tsx b/src/pages/common/components/CommonTabPanels/components/MembersTab/components/Members/Members.tsx index 3adee34ffe..2dd733f836 100644 --- a/src/pages/common/components/CommonTabPanels/components/MembersTab/components/Members/Members.tsx +++ b/src/pages/common/components/CommonTabPanels/components/MembersTab/components/Members/Members.tsx @@ -30,7 +30,7 @@ const MembersComponent: FC = (props) => { () => [...commonMembers].sort( (commonMember, prevCommonMember) => - prevCommonMember.joinedAt.seconds - commonMember.joinedAt.seconds, + prevCommonMember.joinedAt?.seconds - commonMember.joinedAt?.seconds, ), [commonMembers], ); diff --git a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx index 3da4b2a801..6117a8a25f 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -78,409 +78,421 @@ interface DiscussionFeedCardProps { onInternalLinkClick: (data: InternalLinkData) => void; } -const DiscussionFeedCard = forwardRef( - (props, ref) => { - const { - setChatItem, - feedItemIdForAutoChatOpen, - shouldAllowChatAutoOpen, - nestedItemData, - } = useChatContext(); - const { notify } = useNotification(); - const { - item, - governanceCircles, - isMobileVersion = false, +function DiscussionFeedCard(props, ref) { + const { + setChatItem, + feedItemIdForAutoChatOpen, + shouldAllowChatAutoOpen, + nestedItemData, + } = useChatContext(); + const { notify } = useNotification(); + const { + item, + governanceCircles, + isMobileVersion = false, + commonId, + commonName, + commonImage, + commonNotion: outerCommonNotion, + pinnedFeedItems, + commonMember, + isProject, + isPinned, + isPreviewMode, + isActive, + isExpanded, + getLastMessage, + getNonAllowedItems, + onActiveItemDataChange, + directParent, + rootCommonId, + feedItemFollow, + shouldPreLoadMessages, + withoutMenu, + onUserClick, + onFeedItemClick, + onInternalLinkClick, + } = 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 { + isShowing: isLinkStreamModalOpen, + onOpen: onLinkStreamModalOpen, + onClose: onLinkStreamModalClose, + } = useModal(false); + const { + isShowing: isUnlinkStreamModalOpen, + onOpen: onUnlinkStreamModalOpen, + onClose: onUnlinkStreamModalClose, + } = useModal(false); + const { + isShowing: isMoveStreamModalOpen, + onOpen: onMoveStreamModalOpen, + onClose: onMoveStreamModalClose, + } = 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 discussionNotion = commonId + ? discussion?.notionByCommon?.[commonId] + : 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 { markFeedItemAsSeen, markFeedItemAsUnseen } = + useUpdateFeedItemSeenState(); + const menuItems = useMenuItems( + { commonId, - commonName, - commonImage, - commonNotion: outerCommonNotion, pinnedFeedItems, + feedItem: item, + discussion, + governanceCircles, commonMember, - isProject, - isPinned, - isPreviewMode, - isActive, - isExpanded, - getLastMessage, - getNonAllowedItems, - onActiveItemDataChange, - directParent, - rootCommonId, feedItemFollow, - shouldPreLoadMessages, + getNonAllowedItems, + feedItemUserMetadata, withoutMenu, - onUserClick, - onFeedItemClick, - onInternalLinkClick, - } = 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 { - isShowing: isLinkStreamModalOpen, - onOpen: onLinkStreamModalOpen, - onClose: onLinkStreamModalClose, - } = useModal(false); - const { - isShowing: isUnlinkStreamModalOpen, - onOpen: onUnlinkStreamModalOpen, - onClose: onUnlinkStreamModalClose, - } = useModal(false); - const { - isShowing: isMoveStreamModalOpen, - onOpen: onMoveStreamModalOpen, - onClose: onMoveStreamModalClose, - } = 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 discussionNotion = commonId - ? discussion?.notionByCommon?.[commonId] - : 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 { markFeedItemAsSeen, markFeedItemAsUnseen } = - useUpdateFeedItemSeenState(); - const menuItems = useMenuItems( - { - commonId, - pinnedFeedItems, - feedItem: item, - discussion, - governanceCircles, - commonMember, - feedItemFollow, - getNonAllowedItems, - feedItemUserMetadata, - withoutMenu, - }, - { - report: onReportModalOpen, - share: () => onShareModalOpen(), - remove: onDeleteModalOpen, - linkStream: onLinkStreamModalOpen, - unlinkStream: onUnlinkStreamModalOpen, - moveStream: onMoveStreamModalOpen, - markFeedItemAsSeen, - markFeedItemAsUnseen, - }, - ); - 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 commonNotion = outerCommonNotion ?? common?.notion; + }, + { + report: onReportModalOpen, + share: () => onShareModalOpen(), + remove: onDeleteModalOpen, + linkStream: onLinkStreamModalOpen, + unlinkStream: onUnlinkStreamModalOpen, + moveStream: onMoveStreamModalOpen, + markFeedItemAsSeen, + markFeedItemAsUnseen, + }, + ); + 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 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, - }, + 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, - ]); + }, + }); + } + }, [ + discussion, + item.id, + item.circleVisibility, + feedItemUserMetadata?.lastSeen, + feedItemUserMetadata?.lastSeenAt, + feedItemUserMetadata?.count, + feedItemUserMetadata?.seenOnce, + feedItemUserMetadata?.seen, + feedItemUserMetadata?.hasUnseenMention, + nestedItemData, + isPreviewMode, + ]); - const onDiscussionDelete = useCallback(async () => { - try { - if (discussion) { - setDeletingInProgress(true); - await DiscussionService.deleteDiscussion(discussion.id); - onDeleteModalClose(); - } - } catch { - notify("Something went wrong"); - } finally { - setDeletingInProgress(false); + const onDiscussionDelete = useCallback(async () => { + try { + if (discussion) { + setDeletingInProgress(true); + await DiscussionService.deleteDiscussion(discussion.id); + onDeleteModalClose(); } - }, [discussion]); + } catch { + notify("Something went wrong"); + } finally { + setDeletingInProgress(false); + } + }, [discussion]); - const preloadDiscussionMessages = useMemo( - () => - debounce< - typeof preloadDiscussionMessagesData.preloadDiscussionMessages - >( - (...args) => - preloadDiscussionMessagesData.preloadDiscussionMessages(...args), - 6000, - ), - [preloadDiscussionMessagesData.preloadDiscussionMessages], - ); + const preloadDiscussionMessages = useMemo( + () => + debounce( + (...args) => + preloadDiscussionMessagesData.preloadDiscussionMessages(...args), + 6000, + ), + [preloadDiscussionMessagesData.preloadDiscussionMessages], + ); - 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 ( + (!isActive || + shouldAllowChatAutoOpen === null || + shouldAllowChatAutoOpen) && + isDiscussionFetched && + isFeedItemUserMetadataFetched && + item.id === feedItemIdForAutoChatOpen && + !isMobileVersion + ) { + handleOpenChat(); + } + }, [ + isDiscussionFetched, + isFeedItemUserMetadataFetched, + shouldAllowChatAutoOpen, + ]); - useEffect(() => { - if (isActive && shouldAllowChatAutoOpen !== null) { - handleOpenChat(); - } - }, [isActive, shouldAllowChatAutoOpen, handleOpenChat]); + 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]); - useEffect(() => { - if ( - shouldPreLoadMessages && - !isActive && - commonId && - item.circleVisibility - ) { - preloadDiscussionMessages(item.circleVisibility); - } - }, [shouldPreLoadMessages, isActive]); + 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]); + useUpdateEffect(() => { + if ( + shouldPreLoadMessages && + !isActive && + commonId && + item.circleVisibility + ) { + preloadDiscussionMessages(item.circleVisibility, true); + } + }, [item.data.lastMessage?.content]); - return ( - <> - - {(isExpanded || isActive) && ( - onUserClick(item.userId))} - discussionCreator={discussionCreator} - isHome={isHome} - menuItems={menuItems} - discussion={discussion} - common={common} - discussionNotion={discussionNotion} - handleOpenChat={handleOpenChat} - onHover={onHover} - isLoading={isLoading} - /> - )} - - {userId && discussion && ( - { + return getLastMessage({ + commonFeedType: item.data.type, + lastMessage: item.data.lastMessage, + discussion, + currentUserId: userId, + feedItemCreatorName: getUserName(discussionCreator), + commonName, + isProject, + hasFiles: item.data.hasFiles, + hasImages: item.data.hasImages, + }); + }, [ + item.data.type, + item.data.lastMessage, + discussion, + userId, + discussionCreator, + commonName, + isProject, + item.data.hasFiles, + item.data.hasImages, + ]); + + return ( + <> + + {(isExpanded || isActive) && ( + onUserClick(item.userId))} + discussionCreator={discussionCreator} + isHome={isHome} + menuItems={menuItems} + discussion={discussion} + common={common} + discussionNotion={discussionNotion} + handleOpenChat={handleOpenChat} + onHover={onHover} + isLoading={isLoading} /> )} - {discussion && ( - + {userId && discussion && ( + + )} + {discussion && ( + + )} + {isDeleteModalOpen && ( + + + + )} + {commonId && ( + <> + - )} - {isDeleteModalOpen && ( - - - - )} - {commonId && ( - <> - - - - - )} - - ); - }, -); + + + + )} + + ); +} -export default DiscussionFeedCard; +export default forwardRef( + DiscussionFeedCard, +); \ No newline at end of file diff --git a/src/pages/common/components/FeedCard/FeedCard.tsx b/src/pages/common/components/FeedCard/FeedCard.tsx index 65f87967a9..8a786f1bdd 100644 --- a/src/pages/common/components/FeedCard/FeedCard.tsx +++ b/src/pages/common/components/FeedCard/FeedCard.tsx @@ -5,6 +5,8 @@ import React, { forwardRef, useImperativeHandle, PropsWithChildren, + useCallback, + useMemo, } from "react"; import { useCollapse } from "react-collapsed"; import classNames from "classnames"; @@ -60,7 +62,7 @@ type FeedCardProps = PropsWithChildren<{ linkedCommonIds?: string[]; }>; -export const FeedCard = forwardRef((props, ref) => { +const FeedCard = (props, ref) => { const { className, feedItemId, @@ -106,11 +108,11 @@ export const FeedCard = forwardRef((props, ref) => { }); const containerRef = useRef(null); - const toggleExpanding = () => { + const toggleExpanding = useCallback(() => { if (setExpandedFeedItemId) { setExpandedFeedItemId(isExpanded ? null : feedItemId); } - }; + }, [setExpandedFeedItemId, isExpanded, feedItemId]); const scrollToTargetTop = ( headerOffset: number, @@ -140,7 +142,7 @@ export const FeedCard = forwardRef((props, ref) => { } }; - const scrollToTargetAdjusted = () => { + const scrollToTargetAdjusted = useCallback(() => { if (scrollTimeoutRef.current) { clearTimeout(scrollTimeoutRef.current); } @@ -192,7 +194,7 @@ export const FeedCard = forwardRef((props, ref) => { }); } }, COLLAPSE_DURATION + EXTRA_WAITING_TIME_FOR_TIMEOUT); - }; + }, [isTabletView]); useEffect(() => { if (isExpanded && containerRef?.current) { @@ -203,63 +205,102 @@ export const FeedCard = forwardRef((props, ref) => { clearTimeout(scrollTimeoutRef.current); scrollTimeoutRef.current = null; } - }, [isExpanded]); + }, [isExpanded, scrollToTargetAdjusted]); - const handleClick = () => { + const handleClick = useCallback(() => { onClick?.(); if (!isTabletView && isActive) { toggleExpanding(); } - }; + }, [onClick, isTabletView, isActive, toggleExpanding]); - const handleExpand: MouseEventHandler = (event) => { - event.stopPropagation(); - toggleExpanding(); - }; + const handleExpand: MouseEventHandler = useCallback( + (event) => { + event.stopPropagation(); + toggleExpanding(); + }, + [toggleExpanding], + ); + + useImperativeHandle( + ref, + () => ({ + itemId: feedItemId, + scrollToItem: scrollToTargetAdjusted, + }), + [feedItemId, scrollToTargetAdjusted], + ); - useImperativeHandle(ref, () => ({ - itemId: feedItemId, - scrollToItem: scrollToTargetAdjusted, - })); + const feedItemBaseContent = useMemo(() => { + return renderFeedItemBaseContent?.({ + lastActivity, + unreadMessages, + isMobileView: isTabletView, + isActive, + isExpanded, + canBeExpanded, + onClick: handleClick, + onExpand: handleExpand, + title, + lastMessage: !isLoading ? lastMessage : undefined, + menuItems, + commonName, + commonId, + image, + imageAlt, + isProject, + isPinned, + isFollowing, + type, + seenOnce, + seen, + ownerId, + discussionPredefinedType, + hasFiles, + hasImages, + hasUnseenMention, + notion, + originalCommonIdForLinking, + linkedCommonIds, + }); + }, [ + lastActivity, + unreadMessages, + isTabletView, + isActive, + isExpanded, + canBeExpanded, + handleClick, + handleExpand, + title, + lastMessage, + isLoading, + menuItems, + commonName, + commonId, + image, + imageAlt, + isProject, + isPinned, + isFollowing, + type, + seenOnce, + seen, + ownerId, + discussionPredefinedType, + hasFiles, + hasImages, + hasUnseenMention, + notion, + originalCommonIdForLinking, + linkedCommonIds, + renderFeedItemBaseContent, + ]); return (
- {!isPreviewMode && ( -
- {renderFeedItemBaseContent?.({ - lastActivity, - unreadMessages, - isMobileView: isTabletView, - isActive, - isExpanded, - canBeExpanded, - onClick: handleClick, - onExpand: handleExpand, - title, - lastMessage: !isLoading ? lastMessage : undefined, - menuItems, - commonName, - commonId, - image, - imageAlt, - isProject, - isPinned, - isFollowing, - type, - seenOnce, - seen, - ownerId, - discussionPredefinedType, - hasFiles, - hasImages, - hasUnseenMention, - notion, - originalCommonIdForLinking, - linkedCommonIds, - })} -
- )} + {!isPreviewMode &&
{feedItemBaseContent}
}
((props, ref) => {
); -}); +}; + +export default forwardRef(FeedCard); diff --git a/src/pages/common/components/FeedCard/index.ts b/src/pages/common/components/FeedCard/index.ts index 71201cbdfb..257e558203 100644 --- a/src/pages/common/components/FeedCard/index.ts +++ b/src/pages/common/components/FeedCard/index.ts @@ -1,4 +1,4 @@ -export * from "./FeedCard"; +export { default as FeedCard } from "./FeedCard"; export * from "./components"; export * from "./types"; export * from "./utils"; diff --git a/src/pages/common/components/FeedItem/FeedItem.tsx b/src/pages/common/components/FeedItem/FeedItem.tsx index 366753b495..56b8afca40 100644 --- a/src/pages/common/components/FeedItem/FeedItem.tsx +++ b/src/pages/common/components/FeedItem/FeedItem.tsx @@ -6,10 +6,7 @@ import React, { useMemo, } from "react"; import { useFeedItemFollow } from "@/shared/hooks/useCases"; -import { - FeedLayoutItemChangeData, - SpaceListVisibility, -} from "@/shared/interfaces"; +import { FeedLayoutItemChangeData } from "@/shared/interfaces"; import { Circles, CirclesPermissions, @@ -20,7 +17,7 @@ import { CommonNotion, DirectParent, } from "@/shared/models"; -import { checkIsItemVisibleForUser, InternalLinkData } from "@/shared/utils"; +import { checkIsItemVisibleForUser } from "@/shared/utils"; import { useFeedItemSubscription } from "../../hooks"; import { DiscussionFeedCard } from "../DiscussionFeedCard"; import { ProposalFeedCard } from "../ProposalFeedCard"; @@ -113,14 +110,20 @@ const FeedItem = forwardRef((props, ref) => { const getNonAllowedItems = outerGetNonAllowedItems || contextGetNonAllowedItems; - const handleUserClick = useMemo( - () => onUserSelect && ((userId: string) => onUserSelect(userId, commonId)), + const handleUserClick = useCallback( + (userId: string) => { + if (onUserSelect) { + onUserSelect(userId, commonId); + } + }, [onUserSelect, commonId], ); const handleActiveItemDataChange = useCallback( (data: FeedLayoutItemChangeData) => { - onActiveItemDataChange?.(data, commonId); + if (onActiveItemDataChange) { + onActiveItemDataChange(data, commonId); + } }, [onActiveItemDataChange, commonId], ); @@ -130,13 +133,80 @@ const FeedItem = forwardRef((props, ref) => { feedItemFollow.isUserFeedItemFollowDataFetched && !feedItemFollow.userFeedItemFollowData ) { - onFeedItemUnfollowed?.(item.id); + if (onFeedItemUnfollowed) { + onFeedItemUnfollowed(item.id); + } } }, [ feedItemFollow.isUserFeedItemFollowDataFetched, feedItemFollow.userFeedItemFollowData, + item.id, + onFeedItemUnfollowed, ]); + + const generalProps = useMemo( + () => ({ + ref, + item, + commonId, + commonName, + commonImage, + commonNotion, + pinnedFeedItems, + isActive, + isExpanded, + isProject, + isPinned, + governanceCircles, + isPreviewMode, + getLastMessage, + commonMember, + getNonAllowedItems, + isMobileVersion, + onActiveItemDataChange: handleActiveItemDataChange, + directParent, + rootCommonId, + feedItemFollow, + onUserSelect, + shouldPreLoadMessages, + withoutMenu, + onUserClick: handleUserClick, + onFeedItemClick, + onInternalLinkClick, + }), + [ + ref, + item, + commonId, + commonName, + commonImage, + commonNotion, + pinnedFeedItems, + isActive, + isExpanded, + isProject, + isPinned, + governanceCircles, + isPreviewMode, + getLastMessage, + commonMember, + getNonAllowedItems, + isMobileVersion, + handleActiveItemDataChange, + directParent, + rootCommonId, + feedItemFollow, + onUserSelect, + shouldPreLoadMessages, + withoutMenu, + handleUserClick, + onFeedItemClick, + onInternalLinkClick, + ], + ); + + if ( shouldCheckItemVisibility && !checkIsItemVisibleForUser({ @@ -150,36 +220,6 @@ const FeedItem = forwardRef((props, ref) => { return null; } - const generalProps = { - ref, - item, - commonId, - commonName, - commonImage, - commonNotion, - pinnedFeedItems, - isActive, - isExpanded, - isProject, - isPinned, - governanceCircles, - isPreviewMode, - getLastMessage, - commonMember, - getNonAllowedItems, - isMobileVersion, - onActiveItemDataChange: handleActiveItemDataChange, - directParent, - rootCommonId, - feedItemFollow, - onUserSelect, - shouldPreLoadMessages, - withoutMenu, - onUserClick: handleUserClick, - onFeedItemClick, - onInternalLinkClick, - }; - if (item.data.type === CommonFeedType.Discussion) { return ; } diff --git a/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx b/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx index 76e3ad9d8c..d909047807 100644 --- a/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx +++ b/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx @@ -2,10 +2,11 @@ import React, { CSSProperties, FC, MouseEventHandler, - ReactNode, useEffect, + useMemo, useRef, useState, + useCallback, } from "react"; import { useCollapse } from "react-collapsed"; import { useSelector } from "react-redux"; @@ -86,52 +87,63 @@ export const ProjectFeedItem: FC = (props) => { duration: COLLAPSE_DURATION, }); const isLoading = !fetched; - const lastMessage = parseStringToTextEditorValue( + const lastMessage = useMemo(() => parseStringToTextEditorValue( `${unreadStreamsCount ?? 0} unread stream${ unreadStreamsCount === 1 ? "" : "s" }`, - ); + ),[unreadStreamsCount]); const commonPath = getCommonPagePath(commonId); const isProject = checkIsProject(common); - const titleEl = ( - <> - {common?.name} - - + const titleEl = useMemo( + () => ( + <> + {common?.name} + + + ), + [common?.name], ); - const handleClick = () => { + const handleClick = useCallback(() => { history.push(commonPath); - }; + }, [history, commonPath]); - const handleExpand: MouseEventHandler = (event) => { - event.stopPropagation(); - setIsExpanded((v) => !v); - }; + const handleExpand: MouseEventHandler = useCallback( + (event) => { + event.stopPropagation(); + setIsExpanded((v) => !v); + }, + [setIsExpanded], + ); - const renderLeftContent = (): ReactNode => ( -
- - ( +
+ + + + - - -
+
+ ), + [common?.image, common?.name, isExpanded, isProject, handleExpand], ); useEffect(() => { @@ -146,6 +158,45 @@ export const ProjectFeedItem: FC = (props) => { } }, [isExpanded]); + const feedItemProps = useMemo( + () => ({ + className: styles.container, + titleWrapperClassName: styles.titleWrapper, + lastActivity: item.updatedAt.seconds * 1000, + isMobileView: isMobileVersion, + title: titleEl, + onClick: handleClick, + onExpand: handleExpand, + seenOnce: true, + isLoading: !isCommonFetched, + unreadMessages, + lastMessage, + seen: !( + unreadStreamsCount && + unreadStreamsCount > 0 && + unreadMessages === 0 + ), + renderLeftContent, + shouldHideBottomContent: !lastMessage, + isFollowing: feedItemFollow.isFollowing, + notion: common?.notion, + }), + [ + item.updatedAt.seconds, + isMobileVersion, + titleEl, + handleClick, + handleExpand, + isCommonFetched, + unreadMessages, + lastMessage, + unreadStreamsCount, + renderLeftContent, + feedItemFollow.isFollowing, + common?.notion, + ], + ); + if ( !isCommonMemberFetched || (!commonMember && common?.listVisibility === SpaceListVisibility.Members) @@ -164,28 +215,7 @@ export const ProjectFeedItem: FC = (props) => { style={itemStyles} >
- {renderFeedItemBaseContent?.({ - className: styles.container, - titleWrapperClassName: styles.titleWrapper, - lastActivity: item.updatedAt.seconds * 1000, - isMobileView: isMobileVersion, - title: titleEl, - onClick: handleClick, - onExpand: handleExpand, - seenOnce: true, - isLoading: !isCommonFetched, - unreadMessages, - lastMessage, - seen: !( - unreadStreamsCount && - unreadStreamsCount > 0 && - unreadMessages === 0 - ), - renderLeftContent, - shouldHideBottomContent: !lastMessage, - isFollowing: feedItemFollow.isFollowing, - notion: common?.notion, - })} + {renderFeedItemBaseContent?.(feedItemProps)}
( } }, [item.data.lastMessage?.content]); + const lastMessage = useMemo(() => { + return 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, + }); + }, [ + item.data.type, + item.data.lastMessage, + discussion, + userId, + feedItemUser, + commonName, + isProject, + item.data.hasFiles, + item.data.hasImages, + ]); + return ( <> ( 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, - })} + lastMessage={lastMessage} canBeExpanded={discussion?.predefinedType !== PredefinedTypes.General} isPreviewMode={isPreviewMode} commonName={commonName} @@ -468,4 +482,4 @@ const ProposalFeedCard = forwardRef( }, ); -export default ProposalFeedCard; +export default ProposalFeedCard; \ No newline at end of file diff --git a/src/pages/commonFeed/CommonFeed.tsx b/src/pages/commonFeed/CommonFeed.tsx index 8d85ba7a82..f578d5d96a 100644 --- a/src/pages/commonFeed/CommonFeed.tsx +++ b/src/pages/commonFeed/CommonFeed.tsx @@ -237,11 +237,14 @@ const CommonFeedComponent: FC = (props) => { fetchUserRelatedData(); }; - const fetchMoreCommonFeedItems = (feedItemId?: string) => { - if (hasMoreCommonFeedItems && !isSearchingFeedItems) { - fetchCommonFeedItems(feedItemId); - } - }; + const fetchMoreCommonFeedItems = useCallback( + (feedItemId?: string) => { + if (hasMoreCommonFeedItems && !isSearchingFeedItems) { + fetchCommonFeedItems(feedItemId); + } + }, + [hasMoreCommonFeedItems, isSearchingFeedItems, fetchCommonFeedItems], + ); const renderFeedItemBaseContent = useCallback( (props: FeedItemBaseContentProps) => , @@ -281,7 +284,7 @@ const CommonFeedComponent: FC = (props) => { : onProjectJoinModalOpen : onCommonJoinModalOpen; - const renderChatInput = (): ReactNode => { + const renderChatInput = useCallback((): ReactNode => { if (commonMember) { return; } @@ -333,9 +336,19 @@ const CommonFeedComponent: FC = (props) => { )} ); - }; + }, [ + commonMember, + isJoinPending, + commonData, + isRootCommonMember, + canJoin, + onRootCommonJoinModalOpen, + getCommonPagePath, + parentCommonMember, + onJoinCommon, + ]); - const renderLayoutTabs = (): ReactElement => { + const renderLayoutTabs = useCallback((): ReactElement => { return (
onJoinCommon()}> @@ -343,7 +356,29 @@ const CommonFeedComponent: FC = (props) => {
); - }; + }, [onJoinCommon]); + + const renderContentWrapper = useCallback( + (children: ReactNode, wrapperStyles?: CSSProperties): ReactNode => + outerContentWrapperRenderer({ + children, + wrapperStyles, + commonData: commonData!, + commonMember, + isGlobalDataFetched, + }), + [ + outerContentWrapperRenderer, + commonData, + commonMember, + isGlobalDataFetched, + ], + ); + + const onPullToRefresh = useCallback(() => { + dispatch(cacheActions.clearFeedStateByCommonId(commonId)); + dispatch(commonActions.resetFeedItems()); + }, [dispatch, commonId]); useEffect(() => { if ( @@ -496,6 +531,51 @@ const CommonFeedComponent: FC = (props) => { } }, [commonAction]); + const FeedLayoutTopContent = useMemo(() => { + if (!commonData) { + return null; + } + + return ( + { + dispatch(commonActions.setCommonAction(null)); + }} + > + {(commonAction === CommonAction.NewDiscussion || + commonAction === CommonAction.EditDiscussion) && ( + + )} + {commonAction === CommonAction.NewProposal && ( + + )} + + ); + }, [ + JSON.stringify(commonData), + JSON.stringify(commonMember), + commonAction, + scrollToItemsTop, + ]); + if (!isDataFetched) { const headerEl = renderLoadingHeader ? ( renderLoadingHeader() @@ -538,62 +618,13 @@ const CommonFeedComponent: FC = (props) => { ); } - const onPullToRefresh = () => { - dispatch(cacheActions.clearFeedStateByCommonId(commonId)); - dispatch(commonActions.resetFeedItems()); - }; - - const renderContentWrapper = ( - children: ReactNode, - wrapperStyles?: CSSProperties, - ): ReactNode => - outerContentWrapperRenderer({ - children, - wrapperStyles, - commonData, - commonMember, - isGlobalDataFetched, - }); - return ( <> { - dispatch(commonActions.setCommonAction(null)); - }} - > - {(commonAction === CommonAction.NewDiscussion || - commonAction === CommonAction.EditDiscussion) && ( - - )} - {commonAction === CommonAction.NewProposal && ( - - )} - - } + topContent={FeedLayoutTopContent} common={commonData.common} governance={commonData.governance} commonMember={commonMember} diff --git a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx index 0f730acc8c..3eba160986 100644 --- a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx +++ b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx @@ -1,19 +1,19 @@ import React, { CSSProperties, forwardRef, - ForwardRefRenderFunction, - ReactNode, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, + ReactNode, + ForwardRefRenderFunction, } from "react"; import { useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; import PullToRefresh from "react-simple-pull-to-refresh"; -import { useWindowSize } from "react-use"; +import { useDeepCompareEffect, useWindowSize } from "react-use"; import classNames from "classnames"; import { selectUser } from "@/pages/Auth/store/selectors"; import { useCommonMember } from "@/pages/OldCommon/hooks"; @@ -215,14 +215,51 @@ const FeedLayout: ForwardRefRenderFunction = ( } = useCommonMember({ shouldAutoReset: false, }); - const commonMember = - chatItem?.nestedItemData?.commonMember || - outerCommonMember || - fetchedCommonMember; + const commonMember = useMemo( + () => + chatItem?.nestedItemData?.commonMember || + outerCommonMember || + fetchedCommonMember, + [ + chatItem?.nestedItemData?.commonMember, + outerCommonMember, + fetchedCommonMember, + ], + ); + + const [ + commonMemberForSpecificCommonIds, + setCommonMemberForSpecificCommonIds, + ] = useState({}); + + useDeepCompareEffect(() => { + const chatItemCommonMember = { ...chatItem?.nestedItemData?.commonMember }; + + setCommonMemberForSpecificCommonIds((prevCommonMembers) => { + if (chatItemCommonMember?.commonId) { + prevCommonMembers[chatItemCommonMember.commonId] = chatItemCommonMember; + } + + if (outerCommonMember?.commonId) { + prevCommonMembers[outerCommonMember.commonId] = outerCommonMember; + } + + if (fetchedCommonMember?.commonId) { + prevCommonMembers[fetchedCommonMember.commonId] = fetchedCommonMember; + } + + return prevCommonMembers; + }); + }, [ + fetchedCommonMember, + chatItem?.nestedItemData?.commonMember, + outerCommonMember, + ]); const userForProfile = useUserForProfile(); const governance = chatItem?.nestedItemData - ? fetchedGovernance + ? fetchedGovernance || outerGovernance : outerGovernance || fetchedGovernance; + const [splitPaneRef, setSplitPaneRef] = useState(null); const maxContentSize = settings?.getSplitViewMaxSize?.(windowWidth) ?? @@ -311,9 +348,16 @@ const FeedLayout: ForwardRefRenderFunction = ( ]); const activeFeedItemId = chatItem?.feedItemId || feedItemIdForAutoChatOpen; const sizeKey = `${windowWidth}_${contentWidth}`; - const userCircleIds = useMemo( - () => Object.values(commonMember?.circles.map ?? {}), - [commonMember?.circles.map], + + const getUserCircleIds = useCallback( + (commonId) => { + return Object.values( + commonMemberForSpecificCommonIds[commonId]?.circles.map ?? + commonMember?.circles.map ?? + {}, + ) as string[]; + }, + [commonMemberForSpecificCommonIds, commonMember?.circles.map], ); const selectedFeedItem = useMemo( @@ -830,7 +874,7 @@ const FeedLayout: ForwardRefRenderFunction = ( item={item.feedItem} governanceCircles={governance?.circles} isMobileVersion={isTabletView} - userCircleIds={userCircleIds} + userCircleIds={getUserCircleIds(commonData?.id)} isActive={isActive} isExpanded={item.feedItem.id === expandedFeedItemId} sizeKey={isActive ? sizeKey : undefined} @@ -916,7 +960,9 @@ const FeedLayout: ForwardRefRenderFunction = ( isProject={selectedItemCommonData.isProject} governanceCircles={governance?.circles} selectedFeedItem={selectedFeedItem?.feedItem} - userCircleIds={userCircleIds} + userCircleIds={getUserCircleIds( + selectedItemCommonData.id, + )} isShowFeedItemDetailsModal={isShowFeedItemDetailsModal} sizeKey={sizeKey} isMainModalOpen={Boolean(chatItem)} @@ -972,4 +1018,4 @@ const FeedLayout: ForwardRefRenderFunction = ( ); }; -export default forwardRef(FeedLayout); +export default forwardRef(FeedLayout); \ No newline at end of file diff --git a/src/pages/commonFeed/components/FeedLayout/components/ProfileContent/ProfileContent.tsx b/src/pages/commonFeed/components/FeedLayout/components/ProfileContent/ProfileContent.tsx index 75cdeaaceb..4f1ead7535 100644 --- a/src/pages/commonFeed/components/FeedLayout/components/ProfileContent/ProfileContent.tsx +++ b/src/pages/commonFeed/components/FeedLayout/components/ProfileContent/ProfileContent.tsx @@ -169,7 +169,7 @@ const ProfileContent: FC = (props) => {

Joined {common.name} at{" "} {formatDate( - new Date(commonMember.joinedAt.seconds * 1000), + new Date(commonMember.joinedAt?.seconds * 1000), DateFormat.SuperShortSecondary, )}

diff --git a/src/pages/inbox/BaseInbox.tsx b/src/pages/inbox/BaseInbox.tsx index 4a92ccae17..6b34a11d8c 100644 --- a/src/pages/inbox/BaseInbox.tsx +++ b/src/pages/inbox/BaseInbox.tsx @@ -143,11 +143,11 @@ const InboxPage: FC = (props) => { }); }; - const fetchMoreInboxItems = () => { + const fetchMoreInboxItems = useCallback(() => { if (hasMoreInboxItems && !isSearchingInboxItems && !areInboxItemsLoading) { fetchInboxItems(); } - }; + },[hasMoreInboxItems, isSearchingInboxItems, areInboxItemsLoading]); const renderFeedItemBaseContent = useCallback( (props: FeedItemBaseContentProps) => , diff --git a/src/pages/inbox/components/ChatChannelItem/ChatChannelItem.tsx b/src/pages/inbox/components/ChatChannelItem/ChatChannelItem.tsx index ccb85d5d87..cd9016cf70 100644 --- a/src/pages/inbox/components/ChatChannelItem/ChatChannelItem.tsx +++ b/src/pages/inbox/components/ChatChannelItem/ChatChannelItem.tsx @@ -149,6 +149,8 @@ export const ChatChannelItem: FC = (props) => { } }, [isActive, finalTitle, dmUsers?.[0]?.photoURL, dmUsersNames?.[0]]); + const lastMessage = useMemo(() => getLastMessage(chatChannel.lastMessage), [chatChannel.lastMessage]); + return ( = (props) => { isMobileView={isTabletView} isActive={isActive} title={finalTitle} - lastMessage={getLastMessage(chatChannel.lastMessage)} + lastMessage={lastMessage} canBeExpanded={false} onClick={handleOpenChat} menuItems={menuItems} diff --git a/src/pages/inbox/components/ChatChannelItem/hooks/useMenuItems.tsx b/src/pages/inbox/components/ChatChannelItem/hooks/useMenuItems.tsx index 05095fc9b7..a1f2883cbc 100644 --- a/src/pages/inbox/components/ChatChannelItem/hooks/useMenuItems.tsx +++ b/src/pages/inbox/components/ChatChannelItem/hooks/useMenuItems.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { Message3Icon } from "@/shared/icons"; import { ContextMenuItem as Item } from "@/shared/interfaces"; import { ChatChannelMenuItem } from "../constants"; @@ -15,32 +15,37 @@ export const useMenuItems = ( ): Item[] => { const { chatChannelUserStatus } = options; const { markChatChannelAsSeen, markChatChannelAsUnseen } = actions; - const items: Item[] = [ - { - id: ChatChannelMenuItem.MarkUnread, - text: "Mark as unread", - onClick: async () => { - if (!chatChannelUserStatus) { - return; - } + const items: Item[] = useMemo( + () => [ + { + id: ChatChannelMenuItem.MarkUnread, + text: "Mark as unread", + onClick: async () => { + if (!chatChannelUserStatus) { + return; + } - markChatChannelAsUnseen(chatChannelUserStatus.chatChannelId); + markChatChannelAsUnseen(chatChannelUserStatus.chatChannelId); + }, + icon: , }, - icon: , - }, - { - id: ChatChannelMenuItem.MarkRead, - text: "Mark as read", - onClick: async () => { - if (!chatChannelUserStatus) { - return; - } + { + id: ChatChannelMenuItem.MarkRead, + text: "Mark as read", + onClick: async () => { + if (!chatChannelUserStatus) { + return; + } - markChatChannelAsSeen(chatChannelUserStatus.chatChannelId); + markChatChannelAsSeen(chatChannelUserStatus.chatChannelId); + }, + icon: , }, - icon: , - }, - ]; + ], + [chatChannelUserStatus?.chatChannelId], + ); - return getAllowedItems(items, options); + const menuItems = useMemo(() => getAllowedItems(items, options), [items, options]); + + return menuItems; }; diff --git a/src/shared/components/Chat/ChatMessage/ChatMessage.module.scss b/src/shared/components/Chat/ChatMessage/ChatMessage.module.scss index 149cfb7bc0..83e9e16f68 100644 --- a/src/shared/components/Chat/ChatMessage/ChatMessage.module.scss +++ b/src/shared/components/Chat/ChatMessage/ChatMessage.module.scss @@ -62,7 +62,7 @@ .messageText { padding: 0.5rem 0rem; border-radius: 0.875rem; - background-color: var(--secondary-hover-fill); + background-color: var(--secondary-background); flex-direction: row; box-sizing: border-box; position: relative; diff --git a/src/shared/components/Separator/index.scss b/src/shared/components/Separator/index.scss index 98ee113f28..3f4445bea3 100644 --- a/src/shared/components/Separator/index.scss +++ b/src/shared/components/Separator/index.scss @@ -3,5 +3,5 @@ .general-separator { width: 100%; height: 1px; - background-color: $light-gray-1; + background-color: var(--gentle-stroke); } diff --git a/src/shared/constants/theme.ts b/src/shared/constants/theme.ts index d8682c30b7..03cdedbca4 100644 --- a/src/shared/constants/theme.ts +++ b/src/shared/constants/theme.ts @@ -54,7 +54,7 @@ export const ThemeColorsValues = { [ThemeColors.secondaryBackground]: "#1f2124", [ThemeColors.hoverFill]: "#271d21", [ThemeColors.secondaryHoverFill]: "#432b33", - [ThemeColors.primaryText]: "#ffffff", + [ThemeColors.primaryText]: "#ddd", [ThemeColors.secondaryText]: "#a75a93", [ThemeColors.tertiaryText]: "#001a36", [ThemeColors.quaternaryText]: "#432B33", diff --git a/src/shared/hooks/ModalHook.tsx b/src/shared/hooks/ModalHook.tsx index 3be2cb23a8..465ab89f42 100644 --- a/src/shared/hooks/ModalHook.tsx +++ b/src/shared/hooks/ModalHook.tsx @@ -1,15 +1,15 @@ -import { useState } from "react"; +import { useState, useCallback } from "react"; const useModal = (show: boolean) => { const [isShowing, setIsShowing] = useState(show); - function onClose() { + const onClose = useCallback(() => { setIsShowing(false); - } + }, []); - function onOpen() { + const onOpen = useCallback(() => { setIsShowing(true); - } + }, []); return { isShowing, diff --git a/src/shared/hooks/useCases/useDiscussionMessagesById.ts b/src/shared/hooks/useCases/useDiscussionMessagesById.ts index a2921fb4a1..7e6fc36d32 100644 --- a/src/shared/hooks/useCases/useDiscussionMessagesById.ts +++ b/src/shared/hooks/useCases/useDiscussionMessagesById.ts @@ -1,6 +1,7 @@ import { useState, useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useDeepCompareEffect, useUpdateEffect } from "react-use"; +import { trace } from "firebase/performance"; import { DiscussionMessageService, MESSAGES_NUMBER_IN_BATCH, @@ -20,7 +21,7 @@ import { User, } from "@/shared/models"; import { InternalLinkData } from "@/shared/utils"; -import firebase from "@/shared/utils/firebase"; +import firebase, { perf } from "@/shared/utils/firebase"; import { cacheActions, selectDiscussionMessagesStateByDiscussionId, @@ -153,59 +154,67 @@ export const useDiscussionMessagesById = ({ const fetchRepliedMessages = useCallback( async (messageId: string, endDate: Date): Promise => { - if (state.data?.find((item) => item.id === messageId)) { - return Promise.resolve(); - } + const fetchRepliedMessagesTrace = trace(perf, 'fetchRepliedMessagesTrace'); + try { + fetchRepliedMessagesTrace.start(); - const { - updatedDiscussionMessages, - removedDiscussionMessages, - lastVisibleSnapshot, - } = await DiscussionMessageService.getDiscussionMessagesByEndDate( - discussionId, - lastVisible && lastVisible[discussionId], - endDate, - ); + if (state.data?.find((item) => item.id === messageId)) { + return Promise.resolve(); + } - setLastVisible((prevVisible) => ({ - ...prevVisible, - [discussionId]: lastVisibleSnapshot, - })); - const discussionsWithText = await Promise.all( - updatedDiscussionMessages.map(async (discussionMessage) => { - const isUserDiscussionMessage = - checkIsUserDiscussionMessage(discussionMessage); - const isSystemMessage = - checkIsSystemDiscussionMessage(discussionMessage); + const { + updatedDiscussionMessages, + removedDiscussionMessages, + lastVisibleSnapshot, + } = await DiscussionMessageService.getDiscussionMessagesByEndDate( + discussionId, + lastVisible && lastVisible[discussionId], + endDate, + ); - const parsedText = await getTextFromTextEditorString({ - userId, - ownerId: isUserDiscussionMessage ? discussionMessage.ownerId : null, - textEditorString: discussionMessage.text, - users, - commonId: discussionMessage.commonId, - systemMessage: isSystemMessage ? discussionMessage : undefined, - getCommonPagePath, - getCommonPageAboutTabPath, - directParent, - onUserClick, - onFeedItemClick, - onInternalLinkClick, - }); + setLastVisible((prevVisible) => ({ + ...prevVisible, + [discussionId]: lastVisibleSnapshot, + })); + const discussionsWithText = await Promise.all( + updatedDiscussionMessages.map(async (discussionMessage) => { + const isUserDiscussionMessage = + checkIsUserDiscussionMessage(discussionMessage); + const isSystemMessage = + checkIsSystemDiscussionMessage(discussionMessage); - return { - ...discussionMessage, - parsedText, - }; - }), - ); - dispatch( - cacheActions.updateDiscussionMessagesStateByDiscussionId({ - discussionId, - removedDiscussionMessages, - updatedDiscussionMessages: discussionsWithText, - }), - ); + const parsedText = await getTextFromTextEditorString({ + userId, + ownerId: isUserDiscussionMessage ? discussionMessage.ownerId : null, + textEditorString: discussionMessage.text, + users, + commonId: discussionMessage.commonId, + systemMessage: isSystemMessage ? discussionMessage : undefined, + getCommonPagePath, + getCommonPageAboutTabPath, + directParent, + onUserClick, + onFeedItemClick, + onInternalLinkClick, + }); + + return { + ...discussionMessage, + parsedText, + }; + }), + ); + dispatch( + cacheActions.updateDiscussionMessagesStateByDiscussionId({ + discussionId, + removedDiscussionMessages, + updatedDiscussionMessages: discussionsWithText, + }), + ); + fetchRepliedMessagesTrace.stop(); + } catch(err) { + fetchRepliedMessagesTrace.stop(); + } }, [ state.data, @@ -222,7 +231,7 @@ export const useDiscussionMessagesById = ({ ], ); - const fetchDiscussionMessages = () => { + const fetchDiscussionMessages = useCallback(() => { if ( !discussionId || isEndOfList[discussionId] || @@ -239,6 +248,9 @@ export const useDiscussionMessagesById = ({ } try { + const fetchDiscussionMessagesTrace = trace(perf, 'fetchDiscussionMessages'); + fetchDiscussionMessagesTrace.start(); + DiscussionMessageService.subscribeToDiscussionMessagesByDiscussionId( discussionId, lastVisible && lastVisible[discussionId], @@ -308,10 +320,11 @@ export const useDiscussionMessagesById = ({ setIsBatchLoading(false); }, ); + fetchDiscussionMessagesTrace.stop(); } catch(err) { setIsBatchLoading(false); } - }; + },[discussionId, isEndOfList, state.loading, state.data, isBatchLoading, lastVisible, userId, users, directParent, getCommonPagePath, getCommonPageAboutTabPath, onUserClick, onFeedItemClick, onInternalLinkClick, dispatch]); useDeepCompareEffect(() => { (async () => { diff --git a/src/shared/hooks/useCases/useInboxItems.ts b/src/shared/hooks/useCases/useInboxItems.ts index 9e15e55087..19b5fde240 100644 --- a/src/shared/hooks/useCases/useInboxItems.ts +++ b/src/shared/hooks/useCases/useInboxItems.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { selectUser } from "@/pages/Auth/store/selectors"; import { Logger, UserService } from "@/services"; @@ -87,10 +87,10 @@ export const useInboxItems = ( ); }; - const refetch = () => { + const refetch = useCallback(() => { setNewItemsBatches([]); dispatch(inboxActions.refetchInboxItems(Boolean(unread))); - }; + },[unread, setNewItemsBatches]); const addNewInboxItems = ( data: { diff --git a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/CommonDropdown/components/DesktopCommonDropdown/DesktopCommonDropdown.tsx b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/CommonDropdown/components/DesktopCommonDropdown/DesktopCommonDropdown.tsx index b74bbf19c2..ba44d17354 100644 --- a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/CommonDropdown/components/DesktopCommonDropdown/DesktopCommonDropdown.tsx +++ b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/CommonDropdown/components/DesktopCommonDropdown/DesktopCommonDropdown.tsx @@ -1,4 +1,10 @@ -import React, { FC, MouseEventHandler, useState } from "react"; +import React, { + FC, + MouseEventHandler, + useState, + useMemo, + useCallback, +} from "react"; import { useSelector } from "react-redux"; import classNames from "classnames"; import { ButtonIcon } from "@/shared/components"; @@ -27,51 +33,56 @@ const DesktopCommonDropdown: FC = (props) => { setMenuRerenderHack((value) => !value); }; - const finalItems = items.map((item) => ({ - ...item, - className: classNames(item.className, styles.menuItem, { - [styles.menuItemForCommonCreation]: item.id === CREATE_COMMON_ITEM_ID, - }), - activeClassName: classNames(item.activeClassName, styles.menuItemActive), - text: ( - <> - - {item.text} - - {item.id === activeItemId && ( - - )} - - ), - onClick: (event) => { - event.preventDefault(); - handleItemClick(); + const finalItems = useMemo(() => { + return items.map((item) => ({ + ...item, + className: classNames(item.className, styles.menuItem, { + [styles.menuItemForCommonCreation]: item.id === CREATE_COMMON_ITEM_ID, + }), + activeClassName: classNames(item.activeClassName, styles.menuItemActive), + text: ( + <> + + {item.text} + + {item.id === activeItemId && ( + + )} + + ), + onClick: (event) => { + event.preventDefault(); + handleItemClick(); - if (item.type !== MenuItemType.Link) { - item.onClick(event); - } - }, - })); + if (item.type !== MenuItemType.Link) { + item.onClick(event); + } + }, + })); + }, [items]); - const onClick: MouseEventHandler = (event) => { + const onClick: MouseEventHandler = useCallback((event) => { event.stopPropagation(); - }; + }, []); - const triggerEl = ( - - {isMobileView ? ( - Change - ) : ( - - )} - + const triggerEl = useMemo( + () => ( + + {isMobileView ? ( + Change + ) : ( + + )} + + ), + [onClick, isActive, isMobileView], ); return ( diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/BreadcrumbsItem.module.scss b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/BreadcrumbsItem.module.scss index 76206a70e3..428a4bf37f 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/BreadcrumbsItem.module.scss +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/BreadcrumbsItem.module.scss @@ -29,6 +29,19 @@ } } +.tooltipContent { + font-family: PoppinsSans, sans-serif; + font-size: $moderate-xsmall; + font-weight: normal; + color: var(--breadcrumbs-color); +} + +.li { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + @include tablet { .li { &:first-child { diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/BreadcrumbsItem.tsx b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/BreadcrumbsItem.tsx index 2839262254..37912d262a 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/BreadcrumbsItem.tsx +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/BreadcrumbsItem/BreadcrumbsItem.tsx @@ -1,7 +1,13 @@ -import React, { FC, useRef } from "react"; +import React, { FC, useRef, useMemo } from "react"; import { useHistory } from "react-router"; +import { useMeasure } from "react-use"; import { useRoutesContext } from "@/shared/contexts"; -import { ContextMenuRef } from "@/shared/ui-kit"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, + ContextMenuRef, +} from "@/shared/ui-kit"; import { ProjectsStateItem } from "@/store/states"; import { truncateBreadcrumbName } from "../../utils"; import { BreadcrumbsMenu } from "../BreadcrumbsMenu"; @@ -18,6 +24,9 @@ export interface BreadcrumbsItemProps { onClick?: () => void; } +const PADDING = 24; +const OVERLAY_THRESHOLD = 5; + const BreadcrumbsItem: FC = (props) => { const { activeItem, @@ -30,8 +39,19 @@ const BreadcrumbsItem: FC = (props) => { onClick, } = props; const history = useHistory(); + const containerRef = useRef(null); + const [listRef, { width: listWidth }] = useMeasure(); + const [buttonRef, { width: buttonWidth }] = useMeasure(); + + const hasOverlay = useMemo(() => { + return ( + Math.abs( + Math.floor(listWidth) - Math.floor(buttonWidth + PADDING), + ) >= OVERLAY_THRESHOLD + ); + }, [listWidth, buttonWidth]); + const { getCommonPagePath } = useRoutesContext(); - const containerRef = useRef(null); const contextMenuRef = useRef(null); const handleButtonClick = () => { @@ -47,21 +67,36 @@ const BreadcrumbsItem: FC = (props) => { }; return ( -
  • - - {withMenu && ( - - )} -
  • +
    +
  • + + + + + + {activeItem.name} + + + {withMenu && ( + + )} +
  • +
    ); }; diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/FeedItemBreadcrumbs.tsx b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/FeedItemBreadcrumbs.tsx index eec107a6e0..1bc6ee1895 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/FeedItemBreadcrumbs.tsx +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/FeedItemBreadcrumbs.tsx @@ -1,4 +1,4 @@ -import React, { FC, useEffect } from "react"; +import React, { FC, useEffect, useMemo, useCallback } from "react"; import { useDispatch } from "react-redux"; import { CommonEvent, CommonEventEmitter } from "@/events"; import { CommonService } from "@/services"; @@ -19,22 +19,33 @@ interface FeedItemBreadcrumbsProps { truncate: boolean; } -const FeedItemBreadcrumbs: FC = (props) => { - const { breadcrumbs, itemsWithMenus, truncate } = props; +const FeedItemBreadcrumbs: FC = ({ + breadcrumbs, + itemsWithMenus, + truncate, +}) => { const dispatch = useDispatch(); const goToCreateCommon = useGoToCreateCommon(); const isMobileView = useIsTabletView(); - const breadcrumbsItems = truncate - ? [breadcrumbs.items[0], breadcrumbs.items[breadcrumbs.items.length - 1]] - : breadcrumbs.items; - const handleItemClick = (item: ProjectsStateItem) => { - if (item.rootCommonId) { - dispatch( - commonLayoutActions.resetCurrentCommonIdAndProjects(item.rootCommonId), - ); - } - }; + const breadcrumbsItems = useMemo(() => { + return truncate + ? [breadcrumbs.items[0], breadcrumbs.items[breadcrumbs.items.length - 1]] + : breadcrumbs.items; + }, [breadcrumbs.items, truncate]); + + const handleItemClick = useCallback( + (item: ProjectsStateItem) => { + if (item.rootCommonId) { + dispatch( + commonLayoutActions.resetCurrentCommonIdAndProjects( + item.rootCommonId, + ), + ); + } + }, + [dispatch], + ); useEffect(() => { const commonIds = breadcrumbs.items.map((item) => item.commonId); @@ -55,7 +66,9 @@ const FeedItemBreadcrumbs: FC = (props) => { }); }); - return unsubscribe; + return () => { + unsubscribe(); + }; }, [breadcrumbs.activeItem?.id]); return ( diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/FeedBreadcrumbsItem/FeedBreadcrumbsItem.tsx b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/FeedBreadcrumbsItem/FeedBreadcrumbsItem.tsx index 6daa302210..95b0055525 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/FeedBreadcrumbsItem/FeedBreadcrumbsItem.tsx +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Breadcrumbs/components/FeedItemBreadcrumbs/components/FeedBreadcrumbsItem/FeedBreadcrumbsItem.tsx @@ -1,4 +1,4 @@ -import React, { FC, useMemo } from "react"; +import React, { FC, useMemo, useCallback } from "react"; import { useSelector } from "react-redux"; import { ProjectsStateItem, @@ -21,49 +21,49 @@ const getItemsByParentId = ( ): ProjectsStateItem[] => data.filter((item) => item.directParent?.commonId === parentId); -const FeedBreadcrumbsItem: FC = (props) => { - const { activeItem, ...restProps } = props; +const FeedBreadcrumbsItem: FC = ({ + activeItem, + ...restProps +}) => { const { commons, areCommonsFetched } = useSelector( selectCommonLayoutCommonsState, ); const { projects, areProjectsFetched } = useSelector( selectCommonLayoutProjectsState, ); + const parentCommonId = activeItem.directParent?.commonId; - const baseItems = useMemo( - () => - parentCommonId ? getItemsByParentId(parentCommonId, projects) : commons, - [parentCommonId, projects, commons], - ); + + const baseItems = useMemo(() => { + return parentCommonId + ? getItemsByParentId(parentCommonId, projects) + : commons; + }, [parentCommonId, projects, commons]); + const areItemsLoading = parentCommonId ? !areProjectsFetched : !areCommonsFetched; - const hasParentPermissionToAddProject = useMemo( - () => - (parentCommonId && - ( - commons.find((item) => item.commonId === parentCommonId) || - projects.find((item) => item.commonId === parentCommonId) - )?.hasPermissionToAddProject) ?? - false, - [commons, projects, parentCommonId], - ); - const items = useMemo( - () => - baseItems.length === 0 - ? [activeItem] - : [...baseItems].sort((prevItem, nextItem) => { - if (prevItem.commonId === activeItem.commonId) { - return -1; - } - if (nextItem.commonId === activeItem.commonId) { - return 1; - } - return 0; - }), - [baseItems, activeItem], - ); + const hasParentPermissionToAddProject = useMemo(() => { + if (!parentCommonId) return false; + + const parentItem = + commons.find((item) => item.commonId === parentCommonId) || + projects.find((item) => item.commonId === parentCommonId); + + return parentItem?.hasPermissionToAddProject ?? false; + }, [commons, projects, parentCommonId]); + + const items = useMemo(() => { + if (baseItems.length === 0) { + return [activeItem]; + } + return [...baseItems].sort((prevItem, nextItem) => { + if (prevItem.commonId === activeItem.commonId) return -1; + if (nextItem.commonId === activeItem.commonId) return 1; + return 0; + }); + }, [baseItems, activeItem]); return ( = (props) => { } }, [isActive, hasNestedContent, isOpenedManually, hasActiveChild]); - const handleTriggerToggle = () => { + const handleTriggerToggle = useCallback(() => { + if (!hasNestedContent) { + return; + } + setIsOpenedManually(!isOpen); setIsOpen((value) => !value); - }; + }, [hasNestedContent]); return (
  • = (props) => { level={level} isActive={isActive} isOpen={isOpen} - onToggle={hasNestedContent ? handleTriggerToggle : undefined} + onToggle={handleTriggerToggle} /> {isOpen ? children : null}
  • diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTrigger/TreeItemTrigger.module.scss b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTrigger/TreeItemTrigger.module.scss index 2ddcd75848..0bb9d1668d 100644 --- a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTrigger/TreeItemTrigger.module.scss +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTrigger/TreeItemTrigger.module.scss @@ -36,63 +36,3 @@ .itemDisabled { cursor: not-allowed; } - -.arrowIconButton { - padding: 0.75rem 1.25rem 0.75rem var(--item-arrow-pl); -} -.arrowIconButtonHidden { - visibility: hidden; -} - -.arrowIcon { - width: 0.5rem; - height: 0.625rem; - color: $c-neutrals-600; - transition: transform 0.2s; -} - -.arrowIconOpen { - transform: rotate(90deg); -} - -.image { - flex-shrink: 0; - margin-right: var(--item-image-mr); - width: 1.5rem; - height: 1.5rem; - object-fit: cover; -} -.imageNonRounded { - border-radius: 0.1875rem; -} -.imageRounded { - border-radius: 50%; -} - -.name { - font-family: PoppinsSans, sans-serif; - font-weight: 600; - font-size: $small; - color: var(--item-text-color); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.nameActive { - color: $white; -} - -.notificationsAmount { - height: 1rem; - margin-left: auto; - padding: 0 0.25rem; - display: flex; - align-items: center; - justify-content: center; - font-weight: 600; - font-size: $xxsmall-2; - color: $c-shades-white; - background-color: $c-primary-400; - border-radius: 0.5rem; -} diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTrigger/TreeItemTrigger.tsx b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTrigger/TreeItemTrigger.tsx index acaddb127a..3b62a1039c 100644 --- a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTrigger/TreeItemTrigger.tsx +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTrigger/TreeItemTrigger.tsx @@ -1,11 +1,9 @@ import React, { FC, MouseEventHandler } from "react"; import { NavLink } from "react-router-dom"; import classNames from "classnames"; -import { ButtonIcon } from "@/shared/components/ButtonIcon"; -import { SmallArrowIcon } from "@/shared/icons"; -import { CommonAvatar } from "@/shared/ui-kit"; import { useTreeContext } from "../../context"; import { Item } from "../../types"; +import { TreeItemTriggerContent } from "../TreeItemTriggerContent"; import styles from "./TreeItemTrigger.module.scss"; interface TreeItemTriggerProps { @@ -50,51 +48,6 @@ const TreeItemTrigger: FC = (props) => { className, treeItemTriggerStyles?.container, ); - const contentEl = ( - <> - - - - - - - - {item.name} - - {item.nameRightContent} - {item.rightContent} - {!!item.notificationsAmount && ( - - {item.notificationsAmount} - - )} - - ); if (onItemClick || item.disabled) { return ( @@ -106,7 +59,15 @@ const TreeItemTrigger: FC = (props) => { tabIndex={0} onClick={handleItemClick} > - {contentEl} +
    ); } @@ -118,7 +79,15 @@ const TreeItemTrigger: FC = (props) => { title={item.name} aria-label={`Go to ${item.name}`} > - {contentEl} + ); }; diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTriggerContent/TreeItemTriggerContent.module.scss b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTriggerContent/TreeItemTriggerContent.module.scss new file mode 100644 index 0000000000..c25d08ebba --- /dev/null +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTriggerContent/TreeItemTriggerContent.module.scss @@ -0,0 +1,58 @@ +@import "../../../../../../../../../constants"; + +.arrowIconButton { + padding: 0.75rem 1.25rem 0.75rem var(--item-arrow-pl); + width: 2.25rem; +} +.arrowIconButtonHidden { + visibility: hidden; +} + +.arrowIcon { + width: 0.5rem; + height: 0.625rem; + color: $c-neutrals-600; + transition: transform 0.2s; +} + +.arrowIconOpen { + transform: rotate(90deg); +} + +.image { + flex-shrink: 0; + margin-right: var(--item-image-mr); + width: 1.5rem; + height: 1.5rem; + object-fit: cover; +} +.imageNonRounded { + border-radius: 0.1875rem; +} +.imageRounded { + border-radius: 50%; +} + +.name { + font-family: PoppinsSans, sans-serif; + font-weight: 600; + font-size: $small; + color: var(--item-text-color); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.notificationsAmount { + height: 1rem; + margin-left: auto; + padding: 0 0.25rem; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: $xxsmall-2; + color: $c-shades-white; + background-color: $c-primary-400; + border-radius: 0.5rem; +} diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTriggerContent/TreeItemTriggerContent.tsx b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTriggerContent/TreeItemTriggerContent.tsx new file mode 100644 index 0000000000..e1ee61b4ca --- /dev/null +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTriggerContent/TreeItemTriggerContent.tsx @@ -0,0 +1,75 @@ +import React, { FC } from "react"; +import classNames from "classnames"; +import { ButtonIcon } from "@/shared/components/ButtonIcon"; +import { SmallArrowIcon } from "@/shared/icons"; +import { CommonAvatar } from "@/shared/ui-kit"; +import { TreeItemTriggerStyles } from "../../context"; +import { Item } from "../../types"; +import styles from "./TreeItemTriggerContent.module.scss"; + +interface TreeItemTriggerContentProps { + treeItemTriggerStyles?: TreeItemTriggerStyles; + item: Item; + level: number; + isActive: boolean; + isOpen: boolean; + onToggle?: () => void; + handleToggle: ( + event: React.MouseEvent, + ) => void; +} + +const TreeItemTriggerContent: FC = (props) => { + const { treeItemTriggerStyles, item, level, isOpen, handleToggle, onToggle } = + props; + + return ( + <> + + {item.items && item.items?.length > 0 && ( + + )} + + + + + + {item.name} + + {item.nameRightContent} + {item.rightContent} + {!!item.notificationsAmount && ( + + {item.notificationsAmount} + + )} + + ); +}; + +export default TreeItemTriggerContent; diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTriggerContent/index.ts b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTriggerContent/index.ts new file mode 100644 index 0000000000..1c215a96e9 --- /dev/null +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/TreeItemTriggerContent/index.ts @@ -0,0 +1 @@ +export { default as TreeItemTriggerContent } from "./TreeItemTriggerContent"; \ No newline at end of file diff --git a/src/shared/models/Common.tsx b/src/shared/models/Common.tsx index c5d79b1d13..ca92fefb3a 100644 --- a/src/shared/models/Common.tsx +++ b/src/shared/models/Common.tsx @@ -172,6 +172,7 @@ export interface CommonMember { isFollowing: boolean; streamsUnreadCountByProjectStream?: Record; unreadCountByProjectStream?: Record; + commonId?: string; } export interface CirclesPermissions { diff --git a/src/shared/ui-kit/ImageGallery/components/ImageGalleryModal/ImageGalleryModal.tsx b/src/shared/ui-kit/ImageGallery/components/ImageGalleryModal/ImageGalleryModal.tsx index ae79bd2605..a6b377232f 100644 --- a/src/shared/ui-kit/ImageGallery/components/ImageGalleryModal/ImageGalleryModal.tsx +++ b/src/shared/ui-kit/ImageGallery/components/ImageGalleryModal/ImageGalleryModal.tsx @@ -75,7 +75,7 @@ const ImageGalleryModal: FC = (props) => { } > { + withOverlay?: boolean; +} + const TooltipContent: ForwardRefRenderFunction< HTMLDivElement, - HTMLProps + TooltipContentProps > = (props, propRef) => { + const { withOverlay } = props; const state = useTooltipContext(); const { children, ...containerProps } = state.getFloatingProps(props); const [side] = state.placement.split("-"); @@ -44,35 +49,64 @@ const TooltipContent: ForwardRefRenderFunction< [state.floating, propRef], ); + const divProps = useMemo( + () => ({ + ref, + style: { + position: state.strategy, + top: state.y ?? 0, + left: state.x ?? 0, + visibility: state.x === null ? "hidden" : "visible", + ...props.style, + } as CSSProperties, + ...containerProps, + className: containerClassName, + }), + [ + state.strategy, + state.x, + state.y, + props.style, + containerProps, + containerClassName, + ], + ); + return ( {state.open && ( -
    - {children} -
    - -
    -
    + <> + {withOverlay ? ( + +
    + {children} +
    + +
    +
    +
    + ) : ( +
    + {children} +
    + +
    +
    + )} + )}
    ); }; -export default forwardRef>( +export default forwardRef( TooltipContent, ); diff --git a/src/shared/utils/firebase.tsx b/src/shared/utils/firebase.tsx index b652bbd957..3db4544c9a 100644 --- a/src/shared/utils/firebase.tsx +++ b/src/shared/utils/firebase.tsx @@ -3,6 +3,7 @@ import "firebase/compat/auth"; import "firebase/compat/firestore"; import "firebase/compat/performance"; import "firebase/compat/storage"; +import { getPerformance } from "firebase/performance"; import { local } from "@/config"; import { Environment, REACT_APP_ENV } from "@/shared/constants"; import config from "../../config"; @@ -11,7 +12,7 @@ interface FirebaseError extends Error { code: string; } -firebase.initializeApp(config.firebase); +const app = firebase.initializeApp(config.firebase); if (REACT_APP_ENV === Environment.Local) { firebase.auth().useEmulator(local.firebase.authDomain); @@ -33,6 +34,19 @@ if (REACT_APP_ENV === Environment.Local) { }); } +let perf; +if (typeof window !== "undefined" && typeof window.fetch !== "undefined") { + perf = getPerformance(app); +} else { + perf = { + trace: () => ({ + start: () => {}, + stop: () => {}, + }), + }; +} + +export { perf }; // firebase.firestore.setLogLevel("debug"); export const isFirebaseError = (error: any): error is FirebaseError => diff --git a/src/shared/utils/tests/mockFetch.ts b/src/shared/utils/tests/mockFetch.ts new file mode 100644 index 0000000000..c68b24a4d1 --- /dev/null +++ b/src/shared/utils/tests/mockFetch.ts @@ -0,0 +1,3 @@ +import fetchMock from 'jest-fetch-mock'; + +fetchMock.enableMocks(); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 22f3f316e2..14ceec5b07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8608,6 +8608,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-fetch@^3.0.4: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" @@ -12782,6 +12789,14 @@ jest-extended@^2.0.0: jest-diff "^27.2.5" jest-get-type "^27.0.6" +jest-fetch-mock@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz#31749c456ae27b8919d69824f1c2bd85fe0a1f3b" + integrity sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw== + dependencies: + cross-fetch "^3.0.4" + promise-polyfill "^8.1.3" + jest-get-type@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" @@ -14718,6 +14733,13 @@ node-fetch@^2, node-fetch@^2.1.2, node-fetch@^2.6.1, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-forge@^1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -16398,6 +16420,11 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== +promise-polyfill@^8.1.3: + version "8.3.0" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63" + integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg== + promise.allsettled@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.5.tgz#2443f3d4b2aa8dfa560f6ac2aa6c4ea999d75f53"