diff --git a/src/pages/commonFeed/BaseCommonFeedPage.tsx b/src/pages/commonFeed/BaseCommonFeedPage.tsx index 4ef9d95c7c..cdda4fcfc7 100644 --- a/src/pages/commonFeed/BaseCommonFeedPage.tsx +++ b/src/pages/commonFeed/BaseCommonFeedPage.tsx @@ -10,6 +10,7 @@ const BaseCommonFeedPage: FC< Pick< CommonFeedProps, | "renderContentWrapper" + | "renderLoadingHeader" | "onActiveItemDataChange" | "feedLayoutOuterStyles" | "feedLayoutSettings" diff --git a/src/pages/commonFeed/CommonFeed.tsx b/src/pages/commonFeed/CommonFeed.tsx index a1ed5b2049..0da50b96fa 100644 --- a/src/pages/commonFeed/CommonFeed.tsx +++ b/src/pages/commonFeed/CommonFeed.tsx @@ -29,6 +29,7 @@ import { useRoutesContext } from "@/shared/contexts"; import { useAuthorizedModal, useQueryParams } from "@/shared/hooks"; import { useCommonFeedItems, useUserCommonIds } from "@/shared/hooks/useCases"; import { useCommonPinnedFeedItems } from "@/shared/hooks/useCases/useCommonPinnedFeedItems"; +import { useIsTabletView } from "@/shared/hooks/viewport"; import { RightArrowThinIcon } from "@/shared/icons"; import { checkIsFeedItemFollowLayoutItem, @@ -46,6 +47,7 @@ import { } from "@/shared/utils"; import { commonActions, + commonLayoutActions, selectCommonAction, selectRecentStreamId, selectSharedFeedItem, @@ -74,6 +76,7 @@ export type RenderCommonFeedContentWrapper = (data: { export interface CommonFeedProps { commonId: string; renderContentWrapper: RenderCommonFeedContentWrapper; + renderLoadingHeader?: (() => ReactNode) | null; feedLayoutOuterStyles?: FeedLayoutOuterStyles; feedLayoutSettings?: FeedLayoutSettings; onActiveItemDataChange?: (data: FeedLayoutItemChangeDataWithType) => void; @@ -83,11 +86,13 @@ const CommonFeedComponent: FC = (props) => { const { commonId, renderContentWrapper: outerContentWrapperRenderer, + renderLoadingHeader, feedLayoutOuterStyles, feedLayoutSettings, onActiveItemDataChange, } = props; const { getCommonPagePath, getProfilePagePath } = useRoutesContext(); + const isTabletView = useIsTabletView(); const queryParams = useQueryParams(); const dispatch = useDispatch(); const history = useHistory(); @@ -106,6 +111,7 @@ const CommonFeedComponent: FC = (props) => { const commonAction = useSelector(selectCommonAction); const { data: commonData, + stateRef, fetched: isCommonDataFetched, fetchCommonData, } = useCommonData(userId); @@ -397,13 +403,47 @@ const CommonFeedComponent: FC = (props) => { } }, [rootCommonMember?.id]); + useEffect(() => { + return () => { + const common = stateRef.current?.data?.common; + + dispatch( + commonLayoutActions.setLastCommonFromFeed({ + id: commonId, + data: common + ? { + name: common.name, + image: common.image, + isProject: checkIsProject(common), + memberCount: common.memberCount, + } + : null, + }), + ); + }; + }, [commonId]); + if (!isDataFetched) { + const headerEl = renderLoadingHeader ? ( + renderLoadingHeader() + ) : ( + } + /> + ); + return ( -
- -
+ <> + {headerEl} +
+ +
+ + ); } + if (!commonData) { return ( <> diff --git a/src/pages/commonFeed/CommonFeedPage.module.scss b/src/pages/commonFeed/CommonFeedPage.module.scss index ce5a42e8f7..24441d5776 100644 --- a/src/pages/commonFeed/CommonFeedPage.module.scss +++ b/src/pages/commonFeed/CommonFeedPage.module.scss @@ -11,3 +11,17 @@ .desktopRightPane { top: var(--split-view-top); } + +.headerContentWrapper { + display: none; + + @include tablet { + height: 3.25rem; + padding: 0 1rem; + display: flex; + align-items: center; + border-bottom: 0.0625rem solid $c-light-gray; + overflow: hidden; + box-sizing: border-box; + } +} diff --git a/src/pages/commonFeed/CommonFeedPage.tsx b/src/pages/commonFeed/CommonFeedPage.tsx index baaf8f513f..e5feeb8e05 100644 --- a/src/pages/commonFeed/CommonFeedPage.tsx +++ b/src/pages/commonFeed/CommonFeedPage.tsx @@ -6,6 +6,7 @@ import { MainRoutesProvider } from "@/shared/contexts"; import { MultipleSpacesLayoutPageContent } from "@/shared/layouts"; import { multipleSpacesLayoutActions, + selectCommonLayoutLastCommonFromFeed, selectMultipleSpacesLayoutMainWidth, } from "@/store/states"; import BaseCommonFeedPage, { @@ -16,6 +17,8 @@ import { FeedLayoutOuterStyles, FeedLayoutSettings, HeaderContent, + HeaderCommonContent, + HeaderContentWrapper, } from "./components"; import { useActiveItemDataChange } from "./hooks"; import { generateSplitViewMaxSizeGetter } from "./utils"; @@ -56,6 +59,7 @@ const CommonFeedPage: FC = () => { const { id: commonId } = useParams(); const dispatch = useDispatch(); const layoutMainWidth = useSelector(selectMultipleSpacesLayoutMainWidth); + const lastCommonFromFeed = useSelector(selectCommonLayoutLastCommonFromFeed); const onActiveItemDataChange = useActiveItemDataChange(); const feedLayoutSettings = useMemo( () => ({ @@ -64,6 +68,21 @@ const CommonFeedPage: FC = () => { }), [layoutMainWidth], ); + const lastCommonFromFeedData = lastCommonFromFeed?.data; + + const renderLoadingHeader = lastCommonFromFeedData + ? () => ( + + + + ) + : null; useEffect(() => { dispatch( @@ -84,6 +103,7 @@ const CommonFeedPage: FC = () => { = (props) => { const { className, common, commonMember, governance } = props; - const { getCommonPageAboutTabPath } = useRoutesContext(); const isMobileVersion = useIsTabletView(); const commonFollow = useCommonFollow(common.id, commonMember); - const isProject = checkIsProject(common); const showFollowIcon = commonFollow.isFollowInProgress ? !commonMember?.isFollowing : commonMember?.isFollowing; return ( -
-
- } - /> - - - -
-
-

{common.name}

- {showFollowIcon && } -
-

- {common.memberCount} member{getPluralEnding(common.memberCount)} -

-
-
-
+ +
= (props) => { isMobileVersion={isMobileVersion} />
-
+ ); }; diff --git a/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.module.scss b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.module.scss new file mode 100644 index 0000000000..4a17e06688 --- /dev/null +++ b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.module.scss @@ -0,0 +1,105 @@ +@import "../../../../../../constants"; +@import "../../../../../../styles/sizes"; + +.container { + display: flex; + overflow: hidden; +} + +.openSidenavButton { + display: none; + + @include tablet { + display: flex; + } +} + +.openSidenavIcon { + width: 1.5rem; + height: 1.5rem; + margin-right: 0.625rem; + transform: rotate(180deg); + color: $c-neutrals-600; +} + +.commonLink { + padding: 0 1.5rem 0 1.375rem; + display: flex; + align-items: center; + text-decoration: none; + overflow: hidden; + box-sizing: border-box; + + &:hover { + .commonName { + color: $c-pink-primary; + text-decoration: underline; + } + } + + @include tablet { + padding-left: 0; + padding-right: 0.5rem; + } +} + +.image { + width: 2.125rem; + height: 2.125rem; + margin-right: 0.75rem; + object-fit: cover; + box-sizing: border-box; + + @include tablet { + width: 2rem; + height: 2rem; + margin-right: 0.625rem; + } +} +.imageNonRounded { + border-radius: 0.375rem; +} +.imageRounded { + border-radius: 50%; +} + +.commonInfoWrapper { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.commonMainInfoWrapper { + display: flex; + align-items: center; + column-gap: 8px; +} + +.commonName { + margin: 0; + font-family: PoppinsSans, sans-serif; + font-weight: 500; + font-size: $moderate-small; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + @include tablet { + font-size: $moderate-xsmall; + } +} + +.commonMembersAmount { + margin: 0; + font-size: $mobile-title; + letter-spacing: 0.02em; + color: $c-gray-40; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + @include tablet { + font-family: PoppinsSans, sans-serif; + font-size: $xxsmall-2; + } +} diff --git a/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.tsx b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.tsx new file mode 100644 index 0000000000..468a1685b7 --- /dev/null +++ b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.tsx @@ -0,0 +1,63 @@ +import React, { FC } from "react"; +import { NavLink } from "react-router-dom"; +import classNames from "classnames"; +import { useRoutesContext } from "@/shared/contexts"; +import { RightArrowThinIcon, StarIcon } from "@/shared/icons"; +import { CommonAvatar, TopNavigationOpenSidenavButton } from "@/shared/ui-kit"; +import { getPluralEnding } from "@/shared/utils"; +import styles from "./HeaderCommonContent.module.scss"; + +interface HeaderCommonContentProps { + commonId: string; + commonName: string; + commonImage: string; + isProject: boolean; + memberCount: number; + showFollowIcon?: boolean; +} + +const HeaderCommonContent: FC = (props) => { + const { + commonId, + commonName, + commonImage, + isProject, + memberCount, + showFollowIcon = false, + } = props; + const { getCommonPageAboutTabPath } = useRoutesContext(); + + return ( +
+ } + /> + + + +
+
+

{commonName}

+ {showFollowIcon && } +
+

+ {memberCount} member{getPluralEnding(memberCount)} +

+
+
+
+ ); +}; + +export default HeaderCommonContent; diff --git a/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/index.ts b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/index.ts new file mode 100644 index 0000000000..c704d1dde3 --- /dev/null +++ b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/index.ts @@ -0,0 +1 @@ +export { default as HeaderCommonContent } from "./HeaderCommonContent"; diff --git a/src/pages/commonFeed/components/HeaderContent/components/HeaderContentWrapper/HeaderContentWrapper.module.scss b/src/pages/commonFeed/components/HeaderContent/components/HeaderContentWrapper/HeaderContentWrapper.module.scss new file mode 100644 index 0000000000..9611291e12 --- /dev/null +++ b/src/pages/commonFeed/components/HeaderContent/components/HeaderContentWrapper/HeaderContentWrapper.module.scss @@ -0,0 +1,12 @@ +@import "../../../../../../styles/sizes"; + +.container { + padding-right: 1.5rem; + display: flex; + justify-content: space-between; + + @include tablet { + padding-left: 1rem; + padding-right: 1.375rem; + } +} diff --git a/src/pages/commonFeed/components/HeaderContent/components/HeaderContentWrapper/HeaderContentWrapper.tsx b/src/pages/commonFeed/components/HeaderContent/components/HeaderContentWrapper/HeaderContentWrapper.tsx new file mode 100644 index 0000000000..74ece9cfdd --- /dev/null +++ b/src/pages/commonFeed/components/HeaderContent/components/HeaderContentWrapper/HeaderContentWrapper.tsx @@ -0,0 +1,17 @@ +import React, { FC } from "react"; +import classNames from "classnames"; +import styles from "./HeaderContentWrapper.module.scss"; + +interface HeaderContentWrapperProps { + className?: string; +} + +const HeaderContentWrapper: FC = (props) => { + const { className, children } = props; + + return ( +
{children}
+ ); +}; + +export default HeaderContentWrapper; diff --git a/src/pages/commonFeed/components/HeaderContent/components/HeaderContentWrapper/index.ts b/src/pages/commonFeed/components/HeaderContent/components/HeaderContentWrapper/index.ts new file mode 100644 index 0000000000..8e86f656d4 --- /dev/null +++ b/src/pages/commonFeed/components/HeaderContent/components/HeaderContentWrapper/index.ts @@ -0,0 +1 @@ +export { default as HeaderContentWrapper } from "./HeaderContentWrapper"; diff --git a/src/pages/commonFeed/components/HeaderContent/components/index.ts b/src/pages/commonFeed/components/HeaderContent/components/index.ts index 0f8782fc2a..335a9be924 100644 --- a/src/pages/commonFeed/components/HeaderContent/components/index.ts +++ b/src/pages/commonFeed/components/HeaderContent/components/index.ts @@ -1,3 +1,5 @@ +export * from "./HeaderCommonContent"; +export * from "./HeaderContentWrapper"; export * from "./NewStreamButton"; export * from "./ShareButton"; export * from "./ActionsButton"; diff --git a/src/pages/commonFeed/components/HeaderContent/index.ts b/src/pages/commonFeed/components/HeaderContent/index.ts index c67e178f9f..7902f0b924 100644 --- a/src/pages/commonFeed/components/HeaderContent/index.ts +++ b/src/pages/commonFeed/components/HeaderContent/index.ts @@ -1 +1,2 @@ export { default as HeaderContent } from "./HeaderContent"; +export { HeaderCommonContent, HeaderContentWrapper } from "./components"; diff --git a/src/pages/commonFeed/hooks/useCommonData/index.ts b/src/pages/commonFeed/hooks/useCommonData/index.ts index ca3086e5f5..c2a84ee823 100644 --- a/src/pages/commonFeed/hooks/useCommonData/index.ts +++ b/src/pages/commonFeed/hooks/useCommonData/index.ts @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { RefObject, useCallback, useRef, useState } from "react"; import { last } from "lodash"; import { CommonFeedService, @@ -16,6 +16,7 @@ interface FetchCommonDataOptions { } interface Return extends CombinedState { + stateRef: RefObject; fetchCommonData: (options: FetchCommonDataOptions) => void; resetCommonData: () => void; } @@ -26,6 +27,8 @@ export const useCommonData = (userId?: string): Return => { fetched: false, data: null, }); + const stateRef = useRef(state); + stateRef.current = state; const isLoading = state.loading; const isFetched = state.fetched; const currentCommonId = state.data?.common.id; @@ -115,6 +118,7 @@ export const useCommonData = (userId?: string): Return => { loading: isLoading, fetched: isFetched, data: state.data, + stateRef, fetchCommonData, resetCommonData, }; diff --git a/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/LayoutTabs.tsx b/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/LayoutTabs.tsx index c3f386c4fb..581b04d66f 100644 --- a/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/LayoutTabs.tsx +++ b/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/LayoutTabs.tsx @@ -10,6 +10,7 @@ import { Tab, Tabs } from "@/shared/components"; import { useRoutesContext } from "@/shared/contexts"; import { Avatar2Icon, BlocksIcon, InboxIcon } from "@/shared/icons"; import { openSidenav } from "@/shared/utils"; +import { selectCommonLayoutLastCommonFromFeed } from "@/store/states"; import { LayoutTab } from "../../constants"; import { getActiveLayoutTab, getLayoutTabName } from "./utils"; import styles from "./LayoutTabs.module.scss"; @@ -29,7 +30,11 @@ interface TabConfiguration { const LayoutTabs: FC = (props) => { const { className } = props; const history = useHistory(); - const { getInboxPagePath, getProfilePagePath } = useRoutesContext(); + const { getCommonPagePath, getInboxPagePath, getProfilePagePath } = + useRoutesContext(); + const lastCommonIdFromFeed = useSelector( + selectCommonLayoutLastCommonFromFeed, + ); const isAuthenticated = useSelector(authentificated()); const userStreamsWithNotificationsAmount = useSelector( selectUserStreamsWithNotificationsAmount(), @@ -67,6 +72,14 @@ const LayoutTabs: FC = (props) => { "--items-amount": tabs.length, } as CSSProperties; + const handleSpacesClick = () => { + if (lastCommonIdFromFeed) { + history.push(getCommonPagePath(lastCommonIdFromFeed.id)); + } else { + openSidenav(); + } + }; + const handleTabChange = (value: unknown) => { if (activeTab === value) { return; @@ -74,7 +87,7 @@ const LayoutTabs: FC = (props) => { switch (value) { case LayoutTab.Spaces: - openSidenav(); + handleSpacesClick(); break; case LayoutTab.Inbox: history.push(getInboxPagePath()); diff --git a/src/store/states/commonLayout/actions.ts b/src/store/states/commonLayout/actions.ts index 93078619a1..d848e9cfef 100644 --- a/src/store/states/commonLayout/actions.ts +++ b/src/store/states/commonLayout/actions.ts @@ -1,6 +1,6 @@ import { createAsyncAction, createStandardAction } from "typesafe-actions"; import { CommonLayoutActionType } from "./constants"; -import { ProjectsStateItem } from "./types"; +import { CommonLayoutState, ProjectsStateItem } from "./types"; export const getCommons = createAsyncAction( CommonLayoutActionType.GET_COMMONS, @@ -33,6 +33,10 @@ export const setCurrentCommonId = createStandardAction( CommonLayoutActionType.SET_CURRENT_COMMON_ID, )(); +export const setLastCommonFromFeed = createStandardAction( + CommonLayoutActionType.SET_LAST_COMMON_FROM_FEED, +)(); + export const clearData = createStandardAction( CommonLayoutActionType.CLEAR_DATA, )(); diff --git a/src/store/states/commonLayout/constants.ts b/src/store/states/commonLayout/constants.ts index 38f9c2f751..5ac407dafb 100644 --- a/src/store/states/commonLayout/constants.ts +++ b/src/store/states/commonLayout/constants.ts @@ -13,6 +13,8 @@ export enum CommonLayoutActionType { SET_CURRENT_COMMON_ID = "@COMMON_LAYOUT/SET_CURRENT_COMMON_ID", + SET_LAST_COMMON_FROM_FEED = "@COMMON_LAYOUT/SET_LAST_COMMON_FROM_FEED", + CLEAR_DATA = "@COMMON_LAYOUT/CLEAR_DATA", CLEAR_DATA_EXCEPT_OF_CURRENT = "@COMMON_LAYOUT/CLEAR_DATA_EXCEPT_OF_CURRENT", diff --git a/src/store/states/commonLayout/reducer.ts b/src/store/states/commonLayout/reducer.ts index 6ee068348b..6a40e72967 100644 --- a/src/store/states/commonLayout/reducer.ts +++ b/src/store/states/commonLayout/reducer.ts @@ -9,6 +9,7 @@ type Action = ActionType; const initialState: CommonLayoutState = { currentCommonId: null, + lastCommonFromFeed: null, commons: [], areCommonsLoading: false, areCommonsFetched: false, @@ -123,6 +124,17 @@ export const reducer = createReducer(initialState) nextState.currentCommonId = payload; }), ) + .handleAction(actions.setLastCommonFromFeed, (state, { payload }) => + produce(state, (nextState) => { + nextState.lastCommonFromFeed = payload && { + ...payload, + data: + nextState.lastCommonFromFeed?.id === payload.id && !payload.data + ? nextState.lastCommonFromFeed?.data + : payload.data, + }; + }), + ) .handleAction(actions.clearData, (state) => produce(state, (nextState) => { clearData(nextState); diff --git a/src/store/states/commonLayout/selectors.ts b/src/store/states/commonLayout/selectors.ts index 4450bf213b..3906927769 100644 --- a/src/store/states/commonLayout/selectors.ts +++ b/src/store/states/commonLayout/selectors.ts @@ -6,6 +6,9 @@ const selectCommonLayout = (state: AppState) => state.commonLayout; export const selectCommonLayoutCommonId = (state: AppState) => state.commonLayout.currentCommonId; +export const selectCommonLayoutLastCommonFromFeed = (state: AppState) => + state.commonLayout.lastCommonFromFeed; + export const selectCommonLayoutCommons = (state: AppState) => state.commonLayout.commons; diff --git a/src/store/states/commonLayout/types.ts b/src/store/states/commonLayout/types.ts index fde59ed117..64af0fd0a8 100644 --- a/src/store/states/commonLayout/types.ts +++ b/src/store/states/commonLayout/types.ts @@ -2,6 +2,15 @@ import { ProjectsStateItem } from "../projects"; export interface CommonLayoutState { currentCommonId: string | null; + lastCommonFromFeed: { + id: string; + data: { + name: string; + image: string; + isProject: boolean; + memberCount: number; + } | null; + } | null; commons: ProjectsStateItem[]; areCommonsLoading: boolean; areCommonsFetched: boolean;