diff --git a/craco.config.js b/craco.config.js index 4398b2d0ed..1a0f6e1601 100644 --- a/craco.config.js +++ b/craco.config.js @@ -25,10 +25,9 @@ module.exports = { { plugin: { overrideWebpackConfig: ({ webpackConfig }) => { - webpackConfig.devtool = - process.env.REACT_APP_ENV === "dev" - ? "source-map" - : "eval-cheap-module-source-map"; + if (process.env.REACT_APP_ENV === "production") { + delete webpackConfig.devtool; + } return webpackConfig; }, diff --git a/package.json b/package.json index 84a2b15b3a..309a040132 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "react-virtualized": "^9.22.3", "react-zoom-pan-pinch": "^3.2.0", "redux": "^4.0.4", + "redux-persist": "^6.0.0", "redux-saga": "^1.1.3", "reselect": "^4.0.0", "slate": "^0.94.1", diff --git a/src/pages/App/AppWrapper.tsx b/src/pages/App/AppWrapper.tsx index 3d8e85deb3..1a769f9468 100644 --- a/src/pages/App/AppWrapper.tsx +++ b/src/pages/App/AppWrapper.tsx @@ -1,11 +1,14 @@ import React, { FC } from "react"; import { Provider } from "react-redux"; -import { store } from "@/shared/appConfig"; +import { PersistGate } from "redux-persist/integration/react"; +import { store, persistor } from "@/shared/appConfig"; import { NotificationProvider } from "./providers"; const AppWrapper: FC = ({ children }) => ( - {children} + + {children} + ); diff --git a/src/pages/Auth/store/saga.tsx b/src/pages/Auth/store/saga.tsx index 6aa36334b2..81641a1cb1 100755 --- a/src/pages/Auth/store/saga.tsx +++ b/src/pages/Auth/store/saga.tsx @@ -15,7 +15,9 @@ import { getProvider } from "@/shared/utils/authProvider"; import { getFundingRequestNotification } from "@/shared/utils/notifications"; import { cacheActions, + commonActions, commonLayoutActions, + inboxActions, multipleSpacesLayoutActions, } from "@/store/states"; import { @@ -301,6 +303,16 @@ const updateUserData = async (user: User): Promise => { }); }; +const resetGlobalData = (fullReset: boolean) => { + if (fullReset) { + store.dispatch(multipleSpacesLayoutActions.resetMultipleSpacesLayout()); + store.dispatch(commonLayoutActions.clearData()); + } + store.dispatch(inboxActions.resetInbox()); + store.dispatch(cacheActions.resetFeedStates()); + store.dispatch(commonActions.resetCommon()); +}; + function* socialLoginSaga({ payload, }: ReturnType) { @@ -465,8 +477,6 @@ function* logOut() { window.ReactNativeWebView.postMessage(WebviewActions.logout); } - yield put(multipleSpacesLayoutActions.resetMultipleSpacesLayout()); - yield put(commonLayoutActions.clearData()); history.push(ROUTE_PATHS.HOME); yield true; } @@ -567,6 +577,12 @@ function* authSagas() { firebase.auth().onAuthStateChanged(async (res) => { try { + const { user: userInStore } = store.getState().auth; + + if (userInStore?.uid !== res?.uid) { + resetGlobalData(!res); + } + store.dispatch( actions.setAuthProvider( getAuthProviderFromProviderData(res?.providerData), diff --git a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx index acdc57a258..5b270c797a 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -12,9 +12,9 @@ import { DeletePrompt, GlobalOverlay, ReportModal } from "@/shared/components"; import { EntityTypes } from "@/shared/constants"; import { useModal, useNotification } from "@/shared/hooks"; import { + FeedItemFollowState, useCommon, useDiscussionById, - useFeedItemFollow, useFeedItemUserMetadata, useUserById, } from "@/shared/hooks/useCases"; @@ -63,6 +63,7 @@ interface DiscussionFeedCardProps { getNonAllowedItems?: GetNonAllowedItemsOptions; onActiveItemDataChange?: (data: FeedLayoutItemChangeData) => void; directParent?: DirectParent | null; + feedItemFollow: FeedItemFollowState; onUserSelect?: (userId: string, commonId?: string) => void; } @@ -89,6 +90,7 @@ const DiscussionFeedCard = forwardRef( getNonAllowedItems, onActiveItemDataChange, directParent, + feedItemFollow, onUserSelect, } = props; const { @@ -124,10 +126,6 @@ const DiscussionFeedCard = forwardRef( fetchFeedItemUserMetadata, } = useFeedItemUserMetadata(); const { data: common } = useCommon(isHome ? commonId : ""); - const feedItemFollow = useFeedItemFollow( - { feedItemId: item.id, commonId }, - { withSubscription: true }, - ); const menuItems = useMenuItems( { commonId, diff --git a/src/pages/common/components/FeedItem/FeedItem.tsx b/src/pages/common/components/FeedItem/FeedItem.tsx index 96b79f19fb..6936c90e34 100644 --- a/src/pages/common/components/FeedItem/FeedItem.tsx +++ b/src/pages/common/components/FeedItem/FeedItem.tsx @@ -1,4 +1,5 @@ -import React, { forwardRef, memo } from "react"; +import React, { forwardRef, memo, useEffect } from "react"; +import { useFeedItemFollow } from "@/shared/hooks/useCases"; import { FeedLayoutItemChangeData } from "@/shared/interfaces"; import { Circles, @@ -64,10 +65,31 @@ const FeedItem = forwardRef((props, ref) => { onActiveItemDataChange, directParent, } = props; - const { onFeedItemUpdate, getLastMessage, getNonAllowedItems, onUserSelect } = - useFeedItemContext(); + const { + onFeedItemUpdate, + onFeedItemUnfollowed, + getLastMessage, + getNonAllowedItems, + onUserSelect, + } = useFeedItemContext(); + const feedItemFollow = useFeedItemFollow( + { feedItemId: item.id, commonId }, + { withSubscription: true }, + ); useFeedItemSubscription(item.id, commonId, onFeedItemUpdate); + useEffect(() => { + if ( + feedItemFollow.isUserFeedItemFollowDataFetched && + !feedItemFollow.userFeedItemFollowData + ) { + onFeedItemUnfollowed?.(item.id); + } + }, [ + feedItemFollow.isUserFeedItemFollowDataFetched, + feedItemFollow.userFeedItemFollowData, + ]); + if ( shouldCheckItemVisibility && !checkIsItemVisibleForUser( @@ -103,6 +125,7 @@ const FeedItem = forwardRef((props, ref) => { isMobileVersion, onActiveItemDataChange: handleActiveItemDataChange, directParent, + feedItemFollow, onUserSelect, }; diff --git a/src/pages/common/components/FeedItem/context.ts b/src/pages/common/components/FeedItem/context.ts index 3f408ee137..0a23aa1ad7 100644 --- a/src/pages/common/components/FeedItem/context.ts +++ b/src/pages/common/components/FeedItem/context.ts @@ -66,6 +66,7 @@ export interface FeedItemContextValue { setExpandedFeedItemId?: (feedItemId: string | null) => void; renderFeedItemBaseContent?: (props: FeedItemBaseContentProps) => ReactNode; onFeedItemUpdate?: (item: CommonFeed, isRemoved: boolean) => void; + onFeedItemUnfollowed?: (itemId: string) => void; feedCardSettings?: FeedCardSettings; getLastMessage: (options: GetLastMessageOptions) => TextEditorValue; getNonAllowedItems?: GetNonAllowedItemsOptions; diff --git a/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx index e585b92953..362fd01ba0 100644 --- a/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx +++ b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx @@ -13,8 +13,8 @@ import { DeletePrompt, GlobalOverlay } from "@/shared/components"; import { useRoutesContext } from "@/shared/contexts"; import { useForceUpdate, useModal, useNotification } from "@/shared/hooks"; import { + FeedItemFollowState, useDiscussionById, - useFeedItemFollow, useFeedItemUserMetadata, useProposalById, useUserById, @@ -80,6 +80,7 @@ interface ProposalFeedCardProps { getLastMessage: (options: GetLastMessageOptions) => TextEditorValue; getNonAllowedItems?: GetNonAllowedItemsOptions; isMobileVersion?: boolean; + feedItemFollow: FeedItemFollowState; onActiveItemDataChange?: (data: FeedLayoutItemChangeData) => void; onUserSelect?: (userId: string, commonId?: string) => void; } @@ -101,6 +102,7 @@ const ProposalFeedCard = forwardRef( getLastMessage, getNonAllowedItems, isMobileVersion, + feedItemFollow, onActiveItemDataChange, onUserSelect, } = props; @@ -175,10 +177,6 @@ const ProposalFeedCard = forwardRef( onOpen: onShareModalOpen, onClose: onShareModalClose, } = useModal(false); - const feedItemFollow = useFeedItemFollow( - { feedItemId: item.id, commonId }, - { withSubscription: true }, - ); const menuItems = useMenuItems( { commonId, diff --git a/src/pages/commonFeed/CommonFeed.module.scss b/src/pages/commonFeed/CommonFeed.module.scss index 443b1de73d..0d4c0298a2 100644 --- a/src/pages/commonFeed/CommonFeed.module.scss +++ b/src/pages/commonFeed/CommonFeed.module.scss @@ -16,8 +16,6 @@ width: 1.5rem; height: 1.5rem; margin-right: 0.625rem; - transform: rotate(180deg); - color: $c-neutrals-600; } .feedLayout { diff --git a/src/pages/commonFeed/CommonFeed.tsx b/src/pages/commonFeed/CommonFeed.tsx index 8182a5d33c..cef65399b3 100644 --- a/src/pages/commonFeed/CommonFeed.tsx +++ b/src/pages/commonFeed/CommonFeed.tsx @@ -29,7 +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 { RightArrowThinIcon } from "@/shared/icons"; +import { SidebarIcon } from "@/shared/icons"; import { checkIsFeedItemFollowLayoutItem, FeedLayoutItem, @@ -45,6 +45,7 @@ import { getCommonPageAboutTabPath, } from "@/shared/utils"; import { + cacheActions, commonActions, commonLayoutActions, selectCommonAction, @@ -162,7 +163,11 @@ const CommonFeedComponent: FC = (props) => { hasMore: hasMoreCommonFeedItems, fetch: fetchCommonFeedItems, batchNumber, - } = useCommonFeedItems(commonId, commonFeedItemIdsForNotListening); + } = useCommonFeedItems( + commonId, + commonFeedItemIdsForNotListening, + sharedFeedItemId, + ); const { isModalOpen: isCommonJoinModalOpen, @@ -330,7 +335,13 @@ const CommonFeedComponent: FC = (props) => { useEffect(() => { fetchData(); + const interval = setInterval(() => { + dispatch(cacheActions.copyFeedStateByCommonId(commonId)); + }, 5000); + return () => { + clearInterval(interval); + dispatch(cacheActions.copyFeedStateByCommonId(commonId)); dispatch(commonActions.resetCommon()); }; }, [commonId]); @@ -405,6 +416,7 @@ const CommonFeedComponent: FC = (props) => { useEffect(() => { return () => { const common = stateRef.current?.data?.common; + const rootCommon = stateRef.current?.data?.rootCommon; dispatch( commonLayoutActions.setLastCommonFromFeed({ @@ -415,6 +427,19 @@ const CommonFeedComponent: FC = (props) => { image: common.image, isProject: checkIsProject(common), memberCount: common.memberCount, + rootCommon: common.rootCommonId + ? { + id: common.rootCommonId, + data: rootCommon + ? { + name: rootCommon.name, + image: rootCommon.image, + isProject: false, + memberCount: rootCommon.memberCount, + } + : null, + } + : null, } : null, }), @@ -428,7 +453,7 @@ const CommonFeedComponent: FC = (props) => { ) : ( } + iconEl={} /> ); @@ -448,7 +473,7 @@ const CommonFeedComponent: FC = (props) => { <> } + iconEl={} />
diff --git a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx index 5ffca2f17b..7adc25a741 100644 --- a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx +++ b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx @@ -119,6 +119,7 @@ interface FeedLayoutProps { renderFeedItemBaseContent: (props: FeedItemBaseContentProps) => ReactNode; renderChatChannelItem?: (props: ChatChannelFeedLayoutItemProps) => ReactNode; onFeedItemUpdate?: (item: CommonFeed, isRemoved: boolean) => void; + onFeedItemUnfollowed?: (itemId: string) => void; getLastMessage: (options: GetLastMessageOptions) => TextEditorValue; sharedFeedItemId?: string | null; emptyText?: string; @@ -159,6 +160,7 @@ const FeedLayout: ForwardRefRenderFunction = ( renderFeedItemBaseContent, renderChatChannelItem, onFeedItemUpdate, + onFeedItemUnfollowed, getLastMessage, sharedFeedItemId, emptyText, @@ -326,6 +328,7 @@ const FeedLayout: ForwardRefRenderFunction = ( setExpandedFeedItemId, renderFeedItemBaseContent, onFeedItemUpdate, + onFeedItemUnfollowed, getLastMessage, getNonAllowedItems, onUserSelect: handleUserWithCommonClick, @@ -333,6 +336,7 @@ const FeedLayout: ForwardRefRenderFunction = ( [ renderFeedItemBaseContent, onFeedItemUpdate, + onFeedItemUnfollowed, getLastMessage, getNonAllowedItems, handleUserWithCommonClick, diff --git a/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.module.scss b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.module.scss index 4a17e06688..54f01fae06 100644 --- a/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.module.scss +++ b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.module.scss @@ -18,14 +18,13 @@ 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; + color: inherit; text-decoration: none; overflow: hidden; box-sizing: border-box; @@ -40,6 +39,13 @@ @include tablet { padding-left: 0; padding-right: 0.5rem; + + &:hover { + .commonName { + color: inherit; + text-decoration: none; + } + } } } diff --git a/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.tsx b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.tsx index 468a1685b7..6f35431e1a 100644 --- a/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.tsx +++ b/src/pages/commonFeed/components/HeaderContent/components/HeaderCommonContent/HeaderCommonContent.tsx @@ -2,7 +2,8 @@ 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 { useIsTabletView } from "@/shared/hooks/viewport"; +import { SidebarIcon, StarIcon } from "@/shared/icons"; import { CommonAvatar, TopNavigationOpenSidenavButton } from "@/shared/ui-kit"; import { getPluralEnding } from "@/shared/utils"; import styles from "./HeaderCommonContent.module.scss"; @@ -26,17 +27,27 @@ const HeaderCommonContent: FC = (props) => { showFollowIcon = false, } = props; const { getCommonPageAboutTabPath } = useRoutesContext(); + const isTabletView = useIsTabletView(); + + const ContentWrapper: FC = ({ children }) => + isTabletView ? ( +
{children}
+ ) : ( + + {children} + + ); return (
} + iconEl={} /> - + = (props) => { {memberCount} member{getPluralEnding(memberCount)}

- +
); }; diff --git a/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.module.scss b/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.module.scss index 7adfd958c3..4a8ea29ac2 100644 --- a/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.module.scss +++ b/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.module.scss @@ -57,8 +57,6 @@ width: 1.5rem; height: 1.5rem; margin-right: 0.625rem; - transform: rotate(180deg); - color: $c-neutrals-600; } .image { diff --git a/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.tsx b/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.tsx index bf4d0ea019..ad7da23405 100644 --- a/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.tsx +++ b/src/pages/commonFeed/components/HeaderContent_v04/HeaderContent_v04.tsx @@ -5,7 +5,7 @@ import { NewStreamButton } from "@/pages/common/components/CommonTabPanels/compo import { useRoutesContext } from "@/shared/contexts"; import { useCommonFollow } from "@/shared/hooks/useCases"; import { useIsTabletView } from "@/shared/hooks/viewport"; -import { RightArrowThinIcon, StarIcon } from "@/shared/icons"; +import { SidebarIcon, StarIcon } from "@/shared/icons"; import { CirclesPermissions, Common, @@ -39,7 +39,7 @@ const HeaderContent_v04: FC = (props) => {
} + iconEl={} /> = (props) => { [dispatch], ); + const handleFeedItemUnfollowed = useCallback( + (itemId: string) => { + dispatch( + inboxActions.updateFeedItem({ + item: { id: itemId }, + isRemoved: true, + }), + ); + }, + [dispatch], + ); + const handleActiveItemChange = useCallback( (activeItemId?: string) => { dispatch(inboxActions.removeEmptyChatChannelItems(activeItemId)); @@ -221,10 +233,6 @@ const InboxPage: FC = (props) => { useEffect(() => { fetchData(); - - return () => { - dispatch(inboxActions.resetInbox()); - }; }, [userId]); useEffect(() => { @@ -271,6 +279,7 @@ const InboxPage: FC = (props) => { renderFeedItemBaseContent={renderFeedItemBaseContent} renderChatChannelItem={renderChatChannelItem} onFeedItemUpdate={handleFeedItemUpdate} + onFeedItemUnfollowed={handleFeedItemUnfollowed} getLastMessage={getLastMessage} sharedFeedItemId={sharedFeedItemId} emptyText={ diff --git a/src/pages/inbox/components/HeaderContent_v04/HeaderContent_v04.tsx b/src/pages/inbox/components/HeaderContent_v04/HeaderContent_v04.tsx index 1697379141..b024903f9b 100644 --- a/src/pages/inbox/components/HeaderContent_v04/HeaderContent_v04.tsx +++ b/src/pages/inbox/components/HeaderContent_v04/HeaderContent_v04.tsx @@ -38,10 +38,6 @@ const HeaderContent_v04: FC = (props) => { isMobileVersion={isMobileVersion} ButtonComponent={PlusButton} /> -
); diff --git a/src/services/Chat.ts b/src/services/Chat.ts index d49b6aa7cd..b46e3a0284 100644 --- a/src/services/Chat.ts +++ b/src/services/Chat.ts @@ -234,6 +234,28 @@ class ChatService { }); }; + public getChatChannels = async (options: { + participantId: string; + startAt?: Timestamp; + endAt?: Timestamp; + }): Promise => { + const { participantId, startAt, endAt } = options; + let query = this.getChatChannelCollection() + .where("participants", "array-contains", participantId) + .orderBy("updatedAt", "desc"); + + if (startAt) { + query = query.startAt(startAt); + } + if (endAt) { + query = query.endAt(endAt); + } + + const snapshot = await query.get(); + + return snapshot.docs.map((doc) => doc.data()); + }; + public subscribeToNewUpdatedChatChannels = ( participantId: string, endBefore: Timestamp, diff --git a/src/services/Common.ts b/src/services/Common.ts index 7bc06fc7ea..50bd9192f5 100644 --- a/src/services/Common.ts +++ b/src/services/Common.ts @@ -160,6 +160,23 @@ class CommonService { .get() ).docs.map((ref) => ref.ref.path.split("/")[1]); + public subscribeToUserCommonIds = ( + userId: string, + callback: (data: string[]) => void, + ): UnsubscribeFunction => { + const query = firebase + .firestore() + .collectionGroup(SubCollections.Members) + .where("userId", "==", userId); + + return query.onSnapshot((snapshot) => { + const userCommonIds = snapshot.docs.map( + (ref) => ref.ref.path.split("/")[1], + ); + callback(userCommonIds); + }); + }; + public getAllUserCommonMemberInfo = async ( userId: string, ): Promise<(CommonMember & { commonId: string })[]> => { diff --git a/src/services/FeedItemFollows.ts b/src/services/FeedItemFollows.ts index c74bf40d10..98a3d99626 100644 --- a/src/services/FeedItemFollows.ts +++ b/src/services/FeedItemFollows.ts @@ -116,6 +116,29 @@ class FeedItemFollowsService { await Api.post(ApiEndpoint.FollowFeedItem, data, { cancelToken }); }; + public getFollowFeedItems = async (options: { + userId: string; + startAt?: Timestamp; + endAt?: Timestamp; + }): Promise => { + const { userId, startAt, endAt } = options; + let query = this.getFeedItemFollowsSubCollection(userId).orderBy( + "lastActivity", + "desc", + ); + + if (startAt) { + query = query.startAt(startAt); + } + if (endAt) { + query = query.endAt(endAt); + } + + const snapshot = await query.get(); + + return snapshot.docs.map((doc) => doc.data()); + }; + public subscribeToNewUpdatedFollowFeedItem = ( userId: string, endBefore: Timestamp, diff --git a/src/shared/appConfig.ts b/src/shared/appConfig.ts index 2650847bfd..07f6406505 100644 --- a/src/shared/appConfig.ts +++ b/src/shared/appConfig.ts @@ -1,6 +1,6 @@ import configureStore from "@/store"; import history from "./history"; -const { store } = configureStore(history); +const { store, persistor } = configureStore(history); -export { history, store }; +export { history, store, persistor }; diff --git a/src/shared/hooks/useCases/useCommonFeedItems.ts b/src/shared/hooks/useCases/useCommonFeedItems.ts index 3a4a6c2f66..f4f5a87edd 100644 --- a/src/shared/hooks/useCases/useCommonFeedItems.ts +++ b/src/shared/hooks/useCases/useCommonFeedItems.ts @@ -11,6 +11,7 @@ interface Return export const useCommonFeedItems = ( commonId: string, idsForNotListening?: string[], + sharedFeedItemId?: string | null, ): Return => { const dispatch = useDispatch(); const feedItems = useSelector(selectFeedItems); @@ -20,6 +21,7 @@ export const useCommonFeedItems = ( dispatch( commonActions.getFeedItems.request({ commonId, + sharedFeedItemId, feedItemId, limit: 15, }), @@ -67,7 +69,6 @@ export const useCommonFeedItems = ( dispatch( commonActions.getFeedItems.cancel("Cancel feed items fetch on unmount"), ); - dispatch(commonActions.resetFeedItems()); }; }, []); diff --git a/src/shared/hooks/useCases/useFeedItemFollow.ts b/src/shared/hooks/useCases/useFeedItemFollow.ts index 0fc1e02687..3524366d07 100644 --- a/src/shared/hooks/useCases/useFeedItemFollow.ts +++ b/src/shared/hooks/useCases/useFeedItemFollow.ts @@ -2,6 +2,7 @@ import { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; import { selectUser } from "@/pages/Auth/store/selectors"; import { FollowFeedItemAction } from "@/shared/constants"; +import { FeedItemFollow } from "@/shared/models"; import { selectCommonFeedFollows, selectFollowFeedItemMutationState, @@ -15,6 +16,8 @@ export interface FeedItemFollowState { isFollowing: boolean; isDisabled: boolean; onFollowToggle: (action?: FollowFeedItemAction) => void; + isUserFeedItemFollowDataFetched: boolean; + userFeedItemFollowData: FeedItemFollow | null; } interface Data { @@ -105,5 +108,7 @@ export function useFeedItemFollow( isFollowing, isDisabled, onFollowToggle, + isUserFeedItemFollowDataFetched, + userFeedItemFollowData, }; } diff --git a/src/shared/hooks/useCases/useInboxItems.ts b/src/shared/hooks/useCases/useInboxItems.ts index e9329172d4..82805954d4 100644 --- a/src/shared/hooks/useCases/useInboxItems.ts +++ b/src/shared/hooks/useCases/useInboxItems.ts @@ -9,8 +9,13 @@ import { Logger, } from "@/services"; import { InboxItemType } from "@/shared/constants"; +import { useIsMounted } from "@/shared/hooks"; import { FeedLayoutItemWithFollowData } from "@/shared/interfaces"; -import { FeedItemFollow, FeedItemFollowWithMetadata } from "@/shared/models"; +import { + ChatChannel, + FeedItemFollow, + FeedItemFollowWithMetadata, +} from "@/shared/models"; import { inboxActions, InboxItems, selectInboxItems } from "@/store/states"; interface Return @@ -115,6 +120,7 @@ export const useInboxItems = ( options?: { unread?: boolean }, ): Return => { const dispatch = useDispatch(); + const isMounted = useIsMounted(); const [newItemsBatches, setNewItemsBatches] = useState([]); const inboxItems = useSelector(selectInboxItems); const user = useSelector(selectUser()); @@ -135,6 +141,114 @@ export const useInboxItems = ( fetch(); }; + const addNewChatChannels = ( + data: { + chatChannel: ChatChannel; + statuses: { + isAdded: boolean; + isRemoved: boolean; + }; + }[], + ) => { + const finalData = + feedItemIdsForNotListening && feedItemIdsForNotListening.length > 0 + ? data.filter( + (item) => !feedItemIdsForNotListening.includes(item.chatChannel.id), + ) + : data; + + if (finalData.length === 0) { + return; + } + + dispatch( + inboxActions.addNewInboxItems( + finalData.map((item) => ({ + item: { + itemId: item.chatChannel.id, + type: InboxItemType.ChatChannel, + chatChannel: item.chatChannel, + }, + statuses: item.statuses, + })), + ), + ); + }; + + const addNewFollowFeedItems = ( + data: { + item: FeedItemFollow; + statuses: { + isAdded: boolean; + isRemoved: boolean; + }; + }[], + ) => { + if (data.length === 0) { + return; + } + + const finalData = + feedItemIdsForNotListening && feedItemIdsForNotListening.length > 0 + ? data.filter( + (item) => + !feedItemIdsForNotListening.includes(item.item.feedItemId), + ) + : data; + setNewItemsBatches((currentItems) => [...currentItems, finalData]); + }; + + useEffect(() => { + (async () => { + try { + const { firstDocTimestamp: startAt, lastDocTimestamp: endAt } = + inboxItems; + + if (!userId || !startAt || !endAt) { + return; + } + + const [chatChannels, feedItemFollows] = await Promise.all([ + ChatService.getChatChannels({ + participantId: userId, + startAt, + endAt, + }), + FeedItemFollowsService.getFollowFeedItems({ + userId, + startAt, + endAt, + }), + ]); + + if (!isMounted()) { + return; + } + + addNewChatChannels( + chatChannels.map((chatChannel) => ({ + chatChannel, + statuses: { + isAdded: false, + isRemoved: false, + }, + })), + ); + addNewFollowFeedItems( + feedItemFollows.map((item) => ({ + item, + statuses: { + isAdded: false, + isRemoved: false, + }, + })), + ); + } catch (err) { + Logger.error(err); + } + })(); + }, []); + useEffect(() => { if (!inboxItems.firstDocTimestamp || !userId) { return; @@ -144,30 +258,7 @@ export const useInboxItems = ( userId, inboxItems.firstDocTimestamp, (data) => { - const finalData = - feedItemIdsForNotListening && feedItemIdsForNotListening.length > 0 - ? data.filter( - (item) => - !feedItemIdsForNotListening.includes(item.chatChannel.id), - ) - : data; - - if (finalData.length === 0) { - return; - } - - dispatch( - inboxActions.addNewInboxItems( - finalData.map((item) => ({ - item: { - itemId: item.chatChannel.id, - type: InboxItemType.ChatChannel, - chatChannel: item.chatChannel, - }, - statuses: item.statuses, - })), - ), - ); + addNewChatChannels(data); }, ); @@ -184,18 +275,7 @@ export const useInboxItems = ( userId, inboxItems.firstDocTimestamp, (data) => { - if (data.length === 0) { - return; - } - - const finalData = - feedItemIdsForNotListening && feedItemIdsForNotListening.length > 0 - ? data.filter( - (item) => - !feedItemIdsForNotListening.includes(item.item.feedItemId), - ) - : data; - setNewItemsBatches((currentItems) => [...currentItems, finalData]); + addNewFollowFeedItems(data); }, ); diff --git a/src/shared/hooks/useCases/useUserCommonIds.ts b/src/shared/hooks/useCases/useUserCommonIds.ts index 573e9aff85..5b2fbd0ece 100644 --- a/src/shared/hooks/useCases/useUserCommonIds.ts +++ b/src/shared/hooks/useCases/useUserCommonIds.ts @@ -1,13 +1,11 @@ -import { useCallback, useEffect } from "react"; +import { useEffect } from "react"; import { useSelector } from "react-redux"; import { selectUser } from "@/pages/Auth/store/selectors"; -import { CommonService, Logger } from "@/services"; +import { CommonService } from "@/services"; import { LoadingState } from "@/shared/interfaces"; -import { useIsMounted } from "../useIsMounted"; import { useLoadingState } from "../useLoadingState"; export const useUserCommonIds = (): LoadingState => { - const isMounted = useIsMounted(); const user = useSelector(selectUser()); const userId = user?.uid; const [state, setState] = useLoadingState([], { @@ -15,8 +13,13 @@ export const useUserCommonIds = (): LoadingState => { fetched: !userId, }); - const fetchUserCommonIds = useCallback(async () => { + useEffect(() => { if (!userId) { + setState({ + loading: false, + fetched: true, + data: [], + }); return; } @@ -26,37 +29,18 @@ export const useUserCommonIds = (): LoadingState => { data: [], }); - let userCommonIds: string[] = []; - - try { - userCommonIds = await CommonService.getUserCommonIds(userId); - } catch (error) { - Logger.error(error); - } finally { - if (isMounted()) { + const unsubscribe = CommonService.subscribeToUserCommonIds( + userId, + (userCommonIds) => { setState({ loading: false, fetched: true, data: userCommonIds, }); - } - } - }, [userId]); + }, + ); - const setUserCommonIds = useCallback((ids: string[]) => { - setState({ - loading: false, - fetched: true, - data: ids, - }); - }, []); - - useEffect(() => { - if (userId) { - fetchUserCommonIds(); - } else { - setUserCommonIds([]); - } + return unsubscribe; }, [userId]); return { diff --git a/src/shared/icons/blocks2.icon.tsx b/src/shared/icons/blocks2.icon.tsx new file mode 100644 index 0000000000..62378bb383 --- /dev/null +++ b/src/shared/icons/blocks2.icon.tsx @@ -0,0 +1,35 @@ +import React, { FC } from "react"; + +interface Blocks2IconProps { + className?: string; +} + +const Blocks2Icon: FC = ({ className }) => { + const color = "currentColor"; + + return ( + + + + + ); +}; + +export default Blocks2Icon; diff --git a/src/shared/icons/index.ts b/src/shared/icons/index.ts index 47872eacb9..c10cc93f3e 100644 --- a/src/shared/icons/index.ts +++ b/src/shared/icons/index.ts @@ -4,6 +4,7 @@ export { default as Avatar2Icon } from "./avatar2.icon"; export { default as Avatar3Icon } from "./avatar3.icon"; export { default as BillingIcon } from "./billing.icon"; export { default as BlocksIcon } from "./blocks.icon"; +export { default as Blocks2Icon } from "./blocks2.icon"; export { default as BoldMarkIcon } from "./boldMark.icon"; export { default as BoldPlusIcon } from "./boldPlus.icon"; export { default as CaretIcon } from "./caret.icon"; @@ -59,6 +60,7 @@ export { default as InviteFriendsIcon } from "./inviteFriends.icon"; export { default as ShareIcon } from "./share.icon"; export { default as Share2Icon } from "./share2.icon"; export { default as Share3Icon } from "./share3.icon"; +export { default as SidebarIcon } from "./sidebar.icon"; export { default as SendIcon } from "./send.icon"; export { default as SettingsIcon } from "./settings.icon"; export { default as MinusIcon } from "./minus.icon"; diff --git a/src/shared/icons/sidebar.icon.tsx b/src/shared/icons/sidebar.icon.tsx new file mode 100644 index 0000000000..0ee3a31e37 --- /dev/null +++ b/src/shared/icons/sidebar.icon.tsx @@ -0,0 +1,35 @@ +import React, { FC } from "react"; + +interface SidebarIconProps { + className?: string; +} + +const SidebarIcon: FC = ({ className }) => { + const color = "currentColor"; + + return ( + + + + + + ); +}; + +export default SidebarIcon; diff --git a/src/shared/interfaces/feedLayout.ts b/src/shared/interfaces/feedLayout.ts index 48407f0d3e..284d03781a 100644 --- a/src/shared/interfaces/feedLayout.ts +++ b/src/shared/interfaces/feedLayout.ts @@ -5,6 +5,7 @@ import { CommonFeed, FeedItemFollowWithMetadata, } from "@/shared/models"; +import { convertObjectDatesToFirestoreTimestamps } from "@/shared/utils"; export interface FeedLayoutRef { setExpandedFeedItemId: (feedItemId: string | null) => void; @@ -67,3 +68,45 @@ export type FeedLayoutItemChangeDataWithType = FeedLayoutItemChangeData & commonId: string; } ); + +export const deserializeFeedItemFollowLayoutItem = < + T extends FeedItemFollowLayoutItem | FeedItemFollowLayoutItemWithFollowData, +>( + item: T, +): T => ({ + ...item, + feedItem: convertObjectDatesToFirestoreTimestamps(item.feedItem), + feedItemFollowWithMetadata: item.feedItemFollowWithMetadata && { + ...convertObjectDatesToFirestoreTimestamps( + item.feedItemFollowWithMetadata, + ["lastSeen", "lastActivity"], + ), + feedItem: convertObjectDatesToFirestoreTimestamps( + item.feedItemFollowWithMetadata.feedItem, + ), + }, +}); + +export const deserializeChatChannelLayoutItem = ( + item: ChatChannelLayoutItem, +): ChatChannelLayoutItem => ({ + ...item, + chatChannel: convertObjectDatesToFirestoreTimestamps( + item.chatChannel, + ["lastMessage.createdAt"], + ), +}); + +export const deserializeFeedLayoutItem = ( + item: FeedLayoutItem, +): FeedLayoutItem => + checkIsChatChannelLayoutItem(item) + ? deserializeChatChannelLayoutItem(item) + : deserializeFeedItemFollowLayoutItem(item); + +export const deserializeFeedLayoutItemWithFollowData = ( + item: FeedLayoutItemWithFollowData, +): FeedLayoutItemWithFollowData => + checkIsChatChannelLayoutItem(item) + ? deserializeChatChannelLayoutItem(item) + : deserializeFeedItemFollowLayoutItem(item); diff --git a/src/shared/layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss b/src/shared/layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss index c4839e3539..a9bab75222 100644 --- a/src/shared/layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss +++ b/src/shared/layouts/CommonSidenavLayout/CommonSidenavLayout.module.scss @@ -27,7 +27,8 @@ @include tablet { --sb-max-width: 100%; --sb-width: 100%; - --sb-content-width: 100%; + --sb-content-width: 87%; + --sb-shadow: 0.125rem 0 0.375rem #{$c-sidebar-shadow}; --layout-tabs-height: 4rem; } } diff --git a/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/LayoutTabs.tsx b/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/LayoutTabs.tsx index 581b04d66f..d698b0fb55 100644 --- a/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/LayoutTabs.tsx +++ b/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/LayoutTabs.tsx @@ -8,8 +8,10 @@ import { } from "@/pages/Auth/store/selectors"; import { Tab, Tabs } from "@/shared/components"; import { useRoutesContext } from "@/shared/contexts"; -import { Avatar2Icon, BlocksIcon, InboxIcon } from "@/shared/icons"; -import { openSidenav } from "@/shared/utils"; +import { useModal } from "@/shared/hooks"; +import { useUserCommonIds } from "@/shared/hooks/useCases"; +import { Avatar2Icon, Blocks2Icon, InboxIcon } from "@/shared/icons"; +import { CreateCommonPrompt } from "@/shared/layouts/MultipleSpacesLayout/components/Header/components/Navigation/components"; import { selectCommonLayoutLastCommonFromFeed } from "@/store/states"; import { LayoutTab } from "../../constants"; import { getActiveLayoutTab, getLayoutTabName } from "./utils"; @@ -39,6 +41,12 @@ const LayoutTabs: FC = (props) => { const userStreamsWithNotificationsAmount = useSelector( selectUserStreamsWithNotificationsAmount(), ); + const { data: userCommonIds } = useUserCommonIds(); + const { + isShowing: isCreateCommonPromptOpen, + onOpen: onCreateCommonPromptOpen, + onClose: onCreateCommonPromptClose, + } = useModal(false); const finalUserStreamsWithNotificationsAmount = userStreamsWithNotificationsAmount && userStreamsWithNotificationsAmount > 99 @@ -50,7 +58,7 @@ const LayoutTabs: FC = (props) => { { label: getLayoutTabName(LayoutTab.Spaces), value: LayoutTab.Spaces, - icon: , + icon: , }, { label: getLayoutTabName(LayoutTab.Profile), @@ -60,7 +68,7 @@ const LayoutTabs: FC = (props) => { ]; if (isAuthenticated) { - tabs.splice(1, 0, { + tabs.unshift({ label: getLayoutTabName(LayoutTab.Inbox), value: LayoutTab.Inbox, icon: , @@ -73,10 +81,12 @@ const LayoutTabs: FC = (props) => { } as CSSProperties; const handleSpacesClick = () => { - if (lastCommonIdFromFeed) { - history.push(getCommonPagePath(lastCommonIdFromFeed.id)); + const commonForRedirectId = lastCommonIdFromFeed?.id || userCommonIds[0]; + + if (commonForRedirectId) { + history.push(getCommonPagePath(commonForRedirectId)); } else { - openSidenav(); + onCreateCommonPromptOpen(); } }; @@ -101,33 +111,38 @@ const LayoutTabs: FC = (props) => { }; return ( - - {tabs.map((tab) => ( - - {tab.icon} - {typeof tab.notificationsAmount === "number" && ( - - {tab.notificationsAmount} - - )} - - } - includeDefaultMobileStyles={false} - /> - ))} - + <> + + {tabs.map((tab) => ( + + {tab.icon} + {typeof tab.notificationsAmount === "number" && ( + + {tab.notificationsAmount} + + )} + + } + includeDefaultMobileStyles={false} + /> + ))} + + {isCreateCommonPromptOpen && ( + + )} + ); }; diff --git a/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/utils/getLayoutTabName.ts b/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/utils/getLayoutTabName.ts index c21ca54758..1b53dd4722 100644 --- a/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/utils/getLayoutTabName.ts +++ b/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/utils/getLayoutTabName.ts @@ -1,7 +1,7 @@ import { LayoutTab } from "../../../constants"; const LAYOUT_TAB_TO_NAME_MAP: Record = { - [LayoutTab.Spaces]: "Feed", + [LayoutTab.Spaces]: "Spaces", [LayoutTab.Inbox]: "Inbox", [LayoutTab.Profile]: "Profile", }; diff --git a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/SidenavContent.module.scss b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/SidenavContent.module.scss index 81ee25f476..143d213365 100644 --- a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/SidenavContent.module.scss +++ b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/SidenavContent.module.scss @@ -23,14 +23,7 @@ background-color: $c-light-gray-2; @include tablet { - --logo-top-indent: 0; - --logo-right-indent: 0; - --logo-left-indent: 0; - --logo-bottom-indent: 0; - - height: 3.25rem; - align-items: center; - background-color: $c-shades-white; + display: none; } } @@ -44,6 +37,11 @@ } } +.closeIconWrapper { + margin-left: auto; + padding: 1.25rem 1.5rem; +} + .separator { flex-shrink: 0; height: 0.0625rem; @@ -56,10 +54,6 @@ margin-top: auto; } -.layoutTabs { - margin-top: auto; -} - .userInfoContentButton { padding: 1rem 0; diff --git a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/SidenavContent.tsx b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/SidenavContent.tsx index bd684e6b20..c8c2609f1a 100644 --- a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/SidenavContent.tsx +++ b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/SidenavContent.tsx @@ -3,8 +3,9 @@ import { useSelector } from "react-redux"; import classNames from "classnames"; import { authentificated, selectUser } from "@/pages/Auth/store/selectors"; import commonLogoSrc from "@/shared/assets/images/logo-sidenav-2.svg"; +import { ButtonIcon } from "@/shared/components"; import { useIsTabletView } from "@/shared/hooks/viewport"; -import { CommonSidenavLayoutTab } from "@/shared/layouts"; +import { Close2Icon } from "@/shared/icons"; import { CommonLogo } from "@/shared/ui-kit"; import { getUserName } from "@/shared/utils"; import { @@ -13,16 +14,16 @@ import { UserInfo, } from "../../../SidenavLayout/components/SidenavContent"; import { useGoToCreateCommon } from "../../hooks"; -import { LayoutTabs } from "../LayoutTabs"; import { Footer, Navigation, Projects } from "./components"; import styles from "./SidenavContent.module.scss"; interface SidenavContentProps { className?: string; + onClose?: () => void; } const SidenavContent: FC = (props) => { - const { className } = props; + const { className, onClose } = props; const isAuthenticated = useSelector(authentificated()); const user = useSelector(selectUser()); const isTabletView = useIsTabletView(); @@ -41,20 +42,19 @@ const SidenavContent: FC = (props) => { logoClassName={styles.commonLogo} logoSrc={commonLogoSrc} /> - {separatorEl} + {isTabletView && ( + + + + )} {!isTabletView && ( <> + {separatorEl} {separatorEl} )} - {isTabletView && ( - - )} {!isTabletView && ( <>
diff --git a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/Projects/Projects.module.scss b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/Projects/Projects.module.scss index c1938dec1e..c54856fec8 100644 --- a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/Projects/Projects.module.scss +++ b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/Projects/Projects.module.scss @@ -9,16 +9,12 @@ --item-pl-per-level: 1.25rem; --item-arrow-pl: 0.5rem; - height: 3.375rem; + height: 3rem; border-radius: 0; &:hover { --bg-color: #{$c-pink-hover-feed-cards}; } - - @include tablet { - height: 4rem; - } } .projectsTreeItemTriggerActiveClassName { --bg-color: #{$c-pink-active-feed-cards}; diff --git a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/ProjectsTree/hooks/useMenuItems.ts b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/ProjectsTree/hooks/useMenuItems.ts index 1eb5f92517..da65bfff9e 100644 --- a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/ProjectsTree/hooks/useMenuItems.ts +++ b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/components/ProjectsTree/hooks/useMenuItems.ts @@ -24,7 +24,16 @@ export const useMenuItems = (options: Options): MenuItem[] => { onCommonClick(stateItem.commonId); }, })) - .sort((item) => (item.id === activeStateItemId ? -1 : 1)) + .sort((prevItem, nextItem) => { + if (prevItem.id === activeStateItemId) { + return -1; + } + if (nextItem.id === activeStateItemId) { + return 1; + } + + return 0; + }) .concat({ id: CREATE_COMMON_ITEM_ID, text: "Create a common", diff --git a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsData.ts b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsData.ts index 3e7b0fb903..24fdce0ec0 100644 --- a/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsData.ts +++ b/src/shared/layouts/CommonSidenavLayout/components/SidenavContent/hooks/useProjectsData.ts @@ -104,22 +104,16 @@ export const useProjectsData = (): Return => { }, [isAuthenticated]); useEffect(() => { - if (areCommonsLoading) { - return; - } if (!areCommonsFetched) { dispatch(commonLayoutActions.getCommons.request(activeItemId)); } - }, [areCommonsLoading, areCommonsFetched]); + }, [areCommonsFetched]); useEffect(() => { - if (areProjectsLoading || !currentCommonId) { - return; - } - if (!areProjectsFetched) { + if (currentCommonId && !areProjectsFetched) { dispatch(commonLayoutActions.getProjects.request(currentCommonId)); } - }, [areProjectsLoading, areProjectsFetched, currentCommonId]); + }, [areProjectsFetched, currentCommonId]); useEffect(() => { if ( diff --git a/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss b/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss index 64b3e94956..7ab633234d 100644 --- a/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss +++ b/src/shared/layouts/MultipleSpacesLayout/MultipleSpacesLayout.module.scss @@ -30,7 +30,8 @@ --main-pl: unset; --sb-max-width: 100%; --sb-width: 100%; - --sb-content-width: 100%; + --sb-content-width: 87%; + --sb-shadow: 0.125rem 0 0.375rem #{$c-sidebar-shadow}; --layout-tabs-height: 4rem; --header-h: 0; } 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 50b81a54e4..e28802c99f 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 @@ -52,9 +52,16 @@ const FeedBreadcrumbsItem: FC = (props) => { () => baseItems.length === 0 ? [activeItem] - : [...baseItems].sort((prevItem) => - prevItem.commonId === activeItem.commonId ? -1 : 1, - ), + : [...baseItems].sort((prevItem, nextItem) => { + if (prevItem.commonId === activeItem.commonId) { + return -1; + } + if (nextItem.commonId === activeItem.commonId) { + return 1; + } + + return 0; + }), [baseItems, activeItem], ); diff --git a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Navigation/components/CreateCommonPrompt/components/NoCommonsInfo/NoCommonsInfo.module.scss b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Navigation/components/CreateCommonPrompt/components/NoCommonsInfo/NoCommonsInfo.module.scss index 12cabf9c9b..27e90a2667 100644 --- a/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Navigation/components/CreateCommonPrompt/components/NoCommonsInfo/NoCommonsInfo.module.scss +++ b/src/shared/layouts/MultipleSpacesLayout/components/Header/components/Navigation/components/CreateCommonPrompt/components/NoCommonsInfo/NoCommonsInfo.module.scss @@ -1,3 +1,5 @@ +@import "../../../../../../../../../../../styles/sizes"; + .modal { max-width: 26rem; } @@ -10,4 +12,8 @@ .createCommonButton { margin-left: auto; + + @include tablet { + margin-right: auto; + } } diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/PlaceholderTreeItem/PlaceholderTreeItem.module.scss b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/PlaceholderTreeItem/PlaceholderTreeItem.module.scss index 9c10d677ce..36c21583b0 100644 --- a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/PlaceholderTreeItem/PlaceholderTreeItem.module.scss +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/ProjectsTree/components/PlaceholderTreeItem/PlaceholderTreeItem.module.scss @@ -3,7 +3,7 @@ // based on TreeItemTrigger arrow icon button .gap { // {item-arrow-pl} + {arrow-icon-button-pr} + {arrow-icon-button-width} - width: calc(var(--item-arrow-pl) + 0.625rem + 0.375rem); + width: calc(var(--item-arrow-pl) + 1.25rem + 0.5rem); } .image { 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 57d9f17603..933ca60aa0 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,13 +36,15 @@ } .arrowIconButton { - padding: 0.75rem 0.625rem 0.75rem var(--item-arrow-pl); + 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; } diff --git a/src/shared/ui-kit/Sidenav/Sidenav.module.scss b/src/shared/ui-kit/Sidenav/Sidenav.module.scss index fbe0603aa8..b92898b8e8 100644 --- a/src/shared/ui-kit/Sidenav/Sidenav.module.scss +++ b/src/shared/ui-kit/Sidenav/Sidenav.module.scss @@ -1,21 +1,48 @@ @import "../../../constants"; @import "../../../styles/sizes"; +$zIndex: 3; + +.sidenavBackground { + display: none; +} +.sidenavBackgroundOpen { + @include tablet { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: $zIndex; + display: block; + background-color: $c-gray-5; + opacity: 0.5; + animation: fade var(--sb-transition-duration); + } +} + +@keyframes fade { + 0% { + opacity: 0; + } + + 100% { + opacity: 0.5; + } +} + .sidenav { position: fixed; top: 0; bottom: 0; left: 0; - z-index: 3; + z-index: $zIndex; max-width: var(--sb-max-width); width: var(--sb-width); @include tablet { right: 0; - } - - @media (prefers-reduced-motion: reduce) { - --sb-transition-duration: 1ms; + backdrop-filter: blur(0.125rem); } } .sidenavWithAnimation { diff --git a/src/shared/ui-kit/Sidenav/Sidenav.tsx b/src/shared/ui-kit/Sidenav/Sidenav.tsx index 827f0a9962..fcbd7234fd 100644 --- a/src/shared/ui-kit/Sidenav/Sidenav.tsx +++ b/src/shared/ui-kit/Sidenav/Sidenav.tsx @@ -49,34 +49,41 @@ const Sidenav: FC = (props) => { }, [isSidenavOpen]); return ( - + + ); }; diff --git a/src/shared/utils/checkIsSynchronizedDate.ts b/src/shared/utils/checkIsSynchronizedDate.ts new file mode 100644 index 0000000000..b06ed1f63d --- /dev/null +++ b/src/shared/utils/checkIsSynchronizedDate.ts @@ -0,0 +1,4 @@ +import { SynchronizedDate } from "@/shared/interfaces"; + +export const checkIsSynchronizedDate = (date: any): date is SynchronizedDate => + Boolean(date && date._seconds); diff --git a/src/shared/utils/convertDatesToFirestoreTimestamps.ts b/src/shared/utils/convertDatesToFirestoreTimestamps.ts index 4398fed138..ed5022b2f1 100644 --- a/src/shared/utils/convertDatesToFirestoreTimestamps.ts +++ b/src/shared/utils/convertDatesToFirestoreTimestamps.ts @@ -1,11 +1,14 @@ -import firebase from "firebase"; import { get, set } from "lodash"; import { SynchronizedDate } from "@/shared/interfaces"; +import { Timestamp } from "@/shared/models"; +import { checkIsSynchronizedDate } from "@/shared/utils"; export const convertToTimestamp = ( - date: SynchronizedDate, -): firebase.firestore.Timestamp => - new firebase.firestore.Timestamp(date._seconds, date._nanoseconds); + date: SynchronizedDate | Timestamp, +): Timestamp => + checkIsSynchronizedDate(date) + ? new Timestamp(date._seconds, date._nanoseconds) + : new Timestamp(date.seconds, date.nanoseconds); const convertDateInObject = ( data: Record, diff --git a/src/shared/utils/index.tsx b/src/shared/utils/index.tsx index 74e159b1d9..c0151ea0ec 100755 --- a/src/shared/utils/index.tsx +++ b/src/shared/utils/index.tsx @@ -25,6 +25,7 @@ export * from "./routes"; export * from "./checkIsAutomaticJoin"; export * from "./checkIsIFrame"; export * from "./checkIsProject"; +export * from "./checkIsSynchronizedDate"; export * from "./checkIsURL"; export * from "./circles"; export * from "./notifications"; diff --git a/src/store/states/cache/actions.ts b/src/store/states/cache/actions.ts index 12929334cc..e2fa539f88 100644 --- a/src/store/states/cache/actions.ts +++ b/src/store/states/cache/actions.ts @@ -11,6 +11,7 @@ import { User, } from "@/shared/models"; import { CacheActionType } from "./constants"; +import { FeedState } from "./types"; export const getUserStateById = createAsyncAction( CacheActionType.GET_USER_STATE_BY_ID, @@ -132,6 +133,21 @@ export const updateProposalStateById = createStandardAction( state: LoadingState; }>(); +export const copyFeedStateByCommonId = createStandardAction( + CacheActionType.COPY_FEED_STATE_BY_COMMON_ID, +)(); + +export const updateFeedStateByCommonId = createStandardAction( + CacheActionType.UPDATE_FEED_STATE_BY_COMMON_ID, +)<{ + commonId: string; + state: FeedState; +}>(); + +export const resetFeedStates = createStandardAction( + CacheActionType.RESET_FEED_STATES, +)(); + export const getFeedItemUserMetadata = createAsyncAction( CacheActionType.GET_FEED_ITEM_USER_METADATA, CacheActionType.GET_FEED_ITEM_USER_METADATA_SUCCESS, diff --git a/src/store/states/cache/constants.ts b/src/store/states/cache/constants.ts index ae99bfb4f4..53f5ea3784 100644 --- a/src/store/states/cache/constants.ts +++ b/src/store/states/cache/constants.ts @@ -29,6 +29,10 @@ export enum CacheActionType { UPDATE_PROPOSAL_STATE_BY_ID = "@CACHE/UPDATE_PROPOSAL_STATE_BY_ID", + COPY_FEED_STATE_BY_COMMON_ID = "@CACHE/COPY_FEED_STATE_BY_COMMON_ID", + UPDATE_FEED_STATE_BY_COMMON_ID = "@CACHE/UPDATE_FEED_STATE_BY_COMMON_ID", + RESET_FEED_STATES = "@CACHE/RESET_FEED_STATES", + GET_FEED_ITEM_USER_METADATA = "@CACHE/GET_FEED_ITEM_USER_METADATA", GET_FEED_ITEM_USER_METADATA_SUCCESS = "@CACHE/GET_FEED_ITEM_USER_METADATA_SUCCESS", GET_FEED_ITEM_USER_METADATA_FAILURE = "@CACHE/GET_FEED_ITEM_USER_METADATA_FAILURE", diff --git a/src/store/states/cache/reducer.tsx b/src/store/states/cache/reducer.tsx index 8ce7b50b84..d30e54de54 100644 --- a/src/store/states/cache/reducer.tsx +++ b/src/store/states/cache/reducer.tsx @@ -14,6 +14,7 @@ const initialState: CacheState = { discussionStates: {}, discussionMessagesStates: {}, proposalStates: {}, + feedByCommonIdStates: {}, feedItemUserMetadataStates: {}, chatChannelUserStatusStates: {}, }; @@ -69,6 +70,18 @@ export const reducer = createReducer(initialState) nextState.proposalStates[proposalId] = { ...state }; }), ) + .handleAction(actions.updateFeedStateByCommonId, (state, { payload }) => + produce(state, (nextState) => { + const { commonId, state } = payload; + + nextState.feedByCommonIdStates[commonId] = { ...state }; + }), + ) + .handleAction(actions.resetFeedStates, (state) => + produce(state, (nextState) => { + nextState.feedByCommonIdStates = {}; + }), + ) .handleAction(actions.updateFeedItemUserMetadata, (state, { payload }) => produce(state, (nextState) => { const { commonId, userId, feedObjectId, state } = payload; diff --git a/src/store/states/cache/saga/copyFeedStateByCommonId.ts b/src/store/states/cache/saga/copyFeedStateByCommonId.ts new file mode 100644 index 0000000000..ab3405ba70 --- /dev/null +++ b/src/store/states/cache/saga/copyFeedStateByCommonId.ts @@ -0,0 +1,35 @@ +import { put, select } from "redux-saga/effects"; +import { getFeedLayoutItemDateForSorting } from "@/store/states/inbox/utils"; +import { selectCommonState, CommonState } from "../../common"; +import * as actions from "../actions"; + +export function* copyFeedStateByCommonId({ + payload: commonId, +}: ReturnType) { + const commonState = (yield select(selectCommonState)) as CommonState; + const data = + commonState.feedItems.data && commonState.feedItems.data.slice(0, 30); + const feedItems = { + ...commonState.feedItems, + data, + loading: false, + hasMore: true, + firstDocTimestamp: data?.[0] + ? getFeedLayoutItemDateForSorting(data[0]) + : null, + lastDocTimestamp: data?.[data.length - 1] + ? getFeedLayoutItemDateForSorting(data[data.length - 1]) + : null, + }; + + yield put( + actions.updateFeedStateByCommonId({ + commonId, + state: { + feedItems, + pinnedFeedItems: commonState.pinnedFeedItems, + sharedFeedItem: commonState.sharedFeedItem, + }, + }), + ); +} diff --git a/src/store/states/cache/saga/index.ts b/src/store/states/cache/saga/index.ts index 802ac87ead..0eeba6aa13 100644 --- a/src/store/states/cache/saga/index.ts +++ b/src/store/states/cache/saga/index.ts @@ -1,6 +1,8 @@ +import { takeLatest } from "redux-saga/effects"; import { getFeedItemUserMetadataKey } from "@/shared/constants/getFeedItemUserMetadataKey"; import { takeLeadingByIdentifier } from "@/shared/utils/saga"; import * as actions from "../actions"; +import { copyFeedStateByCommonId } from "./copyFeedStateByCommonId"; import { getDiscussionStateById } from "./getDiscussionStateById"; import { getFeedItemUserMetadataState } from "./getFeedItemUserMetadataState"; import { getGovernanceStateByCommonId } from "./getGovernanceStateByCommonId"; @@ -33,4 +35,5 @@ export function* mainSaga() { ({ payload: { payload } }) => getFeedItemUserMetadataKey(payload), getFeedItemUserMetadataState, ); + yield takeLatest(actions.copyFeedStateByCommonId, copyFeedStateByCommonId); } diff --git a/src/store/states/cache/selectors.ts b/src/store/states/cache/selectors.ts index 60ec9ed317..3bfb83d902 100644 --- a/src/store/states/cache/selectors.ts +++ b/src/store/states/cache/selectors.ts @@ -21,6 +21,10 @@ export const selectDiscussionMessagesStateByDiscussionId = (discussionId: string) => (state: AppState) => state.cache.discussionMessagesStates[discussionId] || null; +export const selectFeedStateByCommonId = + (commonId: string) => (state: AppState) => + state.cache.feedByCommonIdStates[commonId] || null; + export const selectFeedItemUserMetadata = (info: { commonId: string; userId: string; feedObjectId: string }) => (state: AppState) => diff --git a/src/store/states/cache/types.ts b/src/store/states/cache/types.ts index 17bf5b0e6f..4d99eec351 100644 --- a/src/store/states/cache/types.ts +++ b/src/store/states/cache/types.ts @@ -8,6 +8,12 @@ import { Proposal, User, } from "@/shared/models"; +import { CommonState } from "../common"; + +export type FeedState = Pick< + CommonState, + "feedItems" | "pinnedFeedItems" | "sharedFeedItem" +>; export interface CacheState { userStates: Record>; @@ -18,6 +24,7 @@ export interface CacheState { string, LoadingState >; + feedByCommonIdStates: Record; // key: {commonId}_{userId}_{feedObjectId} feedItemUserMetadataStates: Record< string, diff --git a/src/store/states/common/actions.ts b/src/store/states/common/actions.ts index a83093fe8e..e71f074cb5 100644 --- a/src/store/states/common/actions.ts +++ b/src/store/states/common/actions.ts @@ -20,7 +20,7 @@ import { Proposal, } from "@/shared/models"; import { CommonActionType } from "./constants"; -import { FeedItems, PinnedFeedItems } from "./types"; +import { CommonState, FeedItems, PinnedFeedItems } from "./types"; export const resetCommon = createStandardAction( CommonActionType.RESET_COMMON, @@ -116,6 +116,7 @@ export const getFeedItems = createAsyncAction( )< { commonId: string; + sharedFeedItemId?: string | null; feedItemId?: string; limit?: number; }, @@ -138,6 +139,13 @@ export const getPinnedFeedItems = createAsyncAction( string >(); +export const setFeedState = createStandardAction( + CommonActionType.SET_FEED_STATE, +)<{ + data: Pick; + sharedFeedItemId?: string | null; +}>(); + export const addNewFeedItems = createStandardAction( CommonActionType.ADD_NEW_FEED_ITEMS, )< diff --git a/src/store/states/common/constants.ts b/src/store/states/common/constants.ts index bbb9cdb0dc..a0e1f79e8a 100644 --- a/src/store/states/common/constants.ts +++ b/src/store/states/common/constants.ts @@ -26,6 +26,8 @@ export enum CommonActionType { GET_PINNED_FEED_ITEMS_FAILURE = "@COMMON/GET_PINNED_FEED_ITEMS_FAILURE", GET_PINNED_FEED_ITEMS_CANCEL = "@COMMON/GET_PINNED_FEED_ITEMS_CANCEL", + SET_FEED_STATE = "@COMMON/SET_FEED_STATE", + ADD_NEW_FEED_ITEMS = "@COMMON/ADD_NEW_FEED_ITEMS", ADD_NEW_PINNED_FEED_ITEMS = "@COMMON/ADD_NEW_PINNED_FEED_ITEMS", diff --git a/src/store/states/common/reducer.ts b/src/store/states/common/reducer.ts index 5e4b5fbd07..94bf974e80 100644 --- a/src/store/states/common/reducer.ts +++ b/src/store/states/common/reducer.ts @@ -2,8 +2,12 @@ import produce from "immer"; import { WritableDraft } from "immer/dist/types/types-external"; import { ActionType, createReducer } from "typesafe-actions"; import { InboxItemType } from "@/shared/constants"; -import { FeedItemFollowLayoutItem } from "@/shared/interfaces"; +import { + deserializeFeedItemFollowLayoutItem, + FeedItemFollowLayoutItem, +} from "@/shared/interfaces"; import { CommonFeed } from "@/shared/models"; +import { convertToTimestamp } from "@/shared/utils"; import * as actions from "./actions"; import { CommonState, FeedItems, PinnedFeedItems } from "./types"; @@ -47,8 +51,7 @@ const initialState: CommonState = { const sortFeedItems = (data: FeedItemFollowLayoutItem[]): void => { data.sort( (prevItem, nextItem) => - nextItem.feedItem.updatedAt.toMillis() - - prevItem.feedItem.updatedAt.toMillis(), + nextItem.feedItem.updatedAt.seconds - prevItem.feedItem.updatedAt.seconds, ); }; @@ -469,6 +472,62 @@ export const reducer = createReducer(initialState) }; }), ) + .handleAction(actions.setFeedState, (state, { payload }) => + produce(state, (nextState) => { + const { + data: { feedItems, pinnedFeedItems, sharedFeedItem }, + sharedFeedItemId, + } = payload; + nextState.feedItems = { + ...feedItems, + data: + feedItems.data && + feedItems.data.map(deserializeFeedItemFollowLayoutItem), + firstDocTimestamp: + (feedItems.firstDocTimestamp && + convertToTimestamp(feedItems.firstDocTimestamp)) || + null, + lastDocTimestamp: + (feedItems.lastDocTimestamp && + convertToTimestamp(feedItems.lastDocTimestamp)) || + null, + hasMore: true, + }; + nextState.pinnedFeedItems = { + ...pinnedFeedItems, + data: + pinnedFeedItems.data && + pinnedFeedItems.data.map(deserializeFeedItemFollowLayoutItem), + }; + + if (sharedFeedItem && sharedFeedItem.itemId === sharedFeedItemId) { + return; + } + if ( + sharedFeedItem && + !pinnedFeedItems.data?.some( + (item) => item.itemId === sharedFeedItem.itemId, + ) && + !feedItems.data?.some((item) => item.itemId === sharedFeedItem.itemId) + ) { + const data = [sharedFeedItem, ...(feedItems.data || [])]; + sortFeedItems(data); + nextState.feedItems.data = data; + } + if (sharedFeedItemId) { + nextState.feedItems.data = + nextState.feedItems.data && + nextState.feedItems.data.filter( + (item) => item.itemId !== sharedFeedItemId, + ); + nextState.pinnedFeedItems.data = + nextState.pinnedFeedItems.data && + nextState.pinnedFeedItems.data.filter( + (item) => item.itemId !== sharedFeedItemId, + ); + } + }), + ) .handleAction(actions.addNewFeedItems, (state, { payload }) => produce(state, (nextState) => { addNewFeedItems(nextState, payload); diff --git a/src/store/states/common/saga/getFeedItems.ts b/src/store/states/common/saga/getFeedItems.ts index 3ab56ccb61..5077852c33 100644 --- a/src/store/states/common/saga/getFeedItems.ts +++ b/src/store/states/common/saga/getFeedItems.ts @@ -3,6 +3,7 @@ import { CommonFeedService } from "@/services"; import { InboxItemType } from "@/shared/constants"; import { Awaited, FeedItemFollowLayoutItem } from "@/shared/interfaces"; import { isError } from "@/shared/utils"; +import { selectFeedStateByCommonId } from "@/store/states"; import * as actions from "../actions"; import { selectFeedItems } from "../selectors"; import { FeedItems } from "../types"; @@ -11,11 +12,23 @@ export function* getFeedItems( action: ReturnType, ) { const { - payload: { commonId, feedItemId, limit }, + payload: { commonId, sharedFeedItemId, feedItemId, limit }, } = action; try { const currentFeedItems = (yield select(selectFeedItems)) as FeedItems; + const cachedFeedState = yield select(selectFeedStateByCommonId(commonId)); + + if (!currentFeedItems.data && !feedItemId && cachedFeedState) { + yield put( + actions.setFeedState({ + data: cachedFeedState, + sharedFeedItemId, + }), + ); + return; + } + const isFirstRequest = !currentFeedItems.lastDocTimestamp; const { data, firstDocTimestamp, lastDocTimestamp, hasMore } = (yield call( CommonFeedService.getCommonFeedItemsByUpdatedAt, diff --git a/src/store/states/common/selectors.ts b/src/store/states/common/selectors.ts index ba61e5c62d..5b1005b293 100644 --- a/src/store/states/common/selectors.ts +++ b/src/store/states/common/selectors.ts @@ -1,5 +1,7 @@ import { AppState } from "@/shared/interfaces"; +export const selectCommonState = (state: AppState) => state.common; + export const selectCommonAction = (state: AppState) => state.common.commonAction; diff --git a/src/store/states/commonLayout/types.ts b/src/store/states/commonLayout/types.ts index 64af0fd0a8..fbe4fc9664 100644 --- a/src/store/states/commonLayout/types.ts +++ b/src/store/states/commonLayout/types.ts @@ -1,16 +1,27 @@ import { ProjectsStateItem } from "../projects"; +interface LastCommonFromFeedData { + name: string; + image: string; + isProject: boolean; + memberCount: number; +} + +interface LastCommonFromFeed { + id: string; + data: + | (LastCommonFromFeedData & { + rootCommon: { + id: string; + data: LastCommonFromFeedData | null; + } | null; + }) + | null; +} + export interface CommonLayoutState { currentCommonId: string | null; - lastCommonFromFeed: { - id: string; - data: { - name: string; - image: string; - isProject: boolean; - memberCount: number; - } | null; - } | null; + lastCommonFromFeed: LastCommonFromFeed | null; commons: ProjectsStateItem[]; areCommonsLoading: boolean; areCommonsFetched: boolean; diff --git a/src/store/states/inbox/reducer.ts b/src/store/states/inbox/reducer.ts index 55d625cfa3..87dca602b7 100644 --- a/src/store/states/inbox/reducer.ts +++ b/src/store/states/inbox/reducer.ts @@ -34,8 +34,8 @@ const initialState: InboxState = { const sortInboxItems = (data: FeedLayoutItemWithFollowData[]): void => { data.sort( (prevItem, nextItem) => - getFeedLayoutItemDateForSorting(nextItem).toMillis() - - getFeedLayoutItemDateForSorting(prevItem).toMillis(), + getFeedLayoutItemDateForSorting(nextItem).seconds - + getFeedLayoutItemDateForSorting(prevItem).seconds, ); }; diff --git a/src/store/store.tsx b/src/store/store.tsx index 9cb6565f86..3e49c96323 100644 --- a/src/store/store.tsx +++ b/src/store/store.tsx @@ -1,3 +1,5 @@ +import { routerMiddleware } from "connected-react-router"; +import { History } from "history"; import { createStore, applyMiddleware, @@ -6,14 +8,32 @@ import { Dispatch, Store, } from "redux"; -import { History } from "history"; -import { routerMiddleware } from "connected-react-router"; import { composeWithDevTools } from "redux-devtools-extension"; import freeze from "redux-freeze"; +import { persistStore, persistReducer, PersistConfig } from "redux-persist"; +import autoMergeLevel2 from "redux-persist/lib/stateReconciler/autoMergeLevel2"; +import storage from "redux-persist/lib/storage"; import createSagaMiddleware from "redux-saga"; -import { AppState } from '@/shared/interfaces'; -import appSagas from "./saga"; +import { AppState } from "@/shared/interfaces"; import rootReducer from "./reducer"; +import appSagas from "./saga"; +import { inboxTransform, lastCommonFromFeedTransform } from "./transforms"; + +const persistConfig: PersistConfig = { + key: "root", + storage, + whitelist: [ + "projects", + "commonLayout", + "commonFeedFollows", + "cache", + "chat", + "inbox", + "multipleSpacesLayout", + ], + stateReconciler: autoMergeLevel2, + transforms: [inboxTransform, lastCommonFromFeedTransform], +}; const sagaMiddleware = createSagaMiddleware(); let middleware: Array; @@ -28,52 +48,65 @@ if (process.env.NODE_ENV === "development") { composer = compose; } -const errorHandlerMiddleware: Middleware = () => (next: Dispatch) => (action) => { - if (action.type.includes("FAILURE")) { - // next( - // showNotification({ - // message: action.payload.error || action.payload.message, - // appearance: "error", - // }), - // ); +const errorHandlerMiddleware: Middleware = + () => (next: Dispatch) => (action) => { + if (action.type.includes("FAILURE")) { + // next( + // showNotification({ + // message: action.payload.error || action.payload.message, + // appearance: "error", + // }), + // ); - if (action.payload && (action.payload.code === 401 || action.payload.code === 403)) { - localStorage.clear(); + if ( + action.payload && + (action.payload.code === 401 || action.payload.code === 403) + ) { + localStorage.clear(); + } } - } - if (action.type.includes("SUCCESS") && action.payload && action.payload.message) { - // next( - // showNotification({ - // message: action.payload.message, - // appearance: "success", - // }), - // ); - } + if ( + action.type.includes("SUCCESS") && + action.payload && + action.payload.message + ) { + // next( + // showNotification({ + // message: action.payload.message, + // appearance: "success", + // }), + // ); + } - return next(action); -}; + return next(action); + }; +// defaults to localStorage for web export default function configureStore(history: History) { + const persistedReducer = persistReducer(persistConfig, rootReducer(history)); const store: Store = createStore( - rootReducer(history), + persistedReducer, undefined, composer( applyMiddleware( ...middleware, routerMiddleware(history), - errorHandlerMiddleware - ) - ) + errorHandlerMiddleware, + ), + ), ); + const persistor = persistStore(store); sagaMiddleware.run(appSagas); // eslint-disable-next-line if ((module as any).hot) { // eslint-disable-next-line - (module as any).hot.accept(() => store.replaceReducer(rootReducer(history))); + (module as any).hot.accept(() => + store.replaceReducer(persistedReducer as any), + ); } - return { store }; + return { store, persistor }; } diff --git a/src/store/transforms.ts b/src/store/transforms.ts new file mode 100644 index 0000000000..11d3e618b8 --- /dev/null +++ b/src/store/transforms.ts @@ -0,0 +1,67 @@ +import { createTransform } from "redux-persist"; +import { deserializeFeedLayoutItemWithFollowData } from "@/shared/interfaces"; +import { convertObjectDatesToFirestoreTimestamps } from "@/shared/utils"; +import { getFeedLayoutItemDateForSorting } from "@/store/states/inbox/utils"; +import { CommonLayoutState } from "./states/commonLayout"; +import { InboxItems, InboxState } from "./states/inbox"; + +export const inboxTransform = createTransform( + (inboundState: InboxState) => { + const data = + inboundState.items.data && inboundState.items.data.slice(0, 30); + + return { + ...inboundState, + items: { + ...inboundState.items, + data, + loading: false, + hasMore: true, + firstDocTimestamp: data?.[0] + ? getFeedLayoutItemDateForSorting(data[0]) + : null, + lastDocTimestamp: data?.[data.length - 1] + ? getFeedLayoutItemDateForSorting(data[data.length - 1]) + : null, + }, + }; + }, + (outboundState: InboxState) => ({ + ...outboundState, + sharedItem: + outboundState.sharedItem && + deserializeFeedLayoutItemWithFollowData(outboundState.sharedItem), + chatChannelItems: [], + items: { + ...convertObjectDatesToFirestoreTimestamps( + outboundState.items, + ["firstDocTimestamp", "lastDocTimestamp"], + ), + data: + outboundState.items.data && + outboundState.items.data.map(deserializeFeedLayoutItemWithFollowData), + }, + }), + { whitelist: ["inbox"] }, +); + +export const lastCommonFromFeedTransform = createTransform( + (inboundState: CommonLayoutState) => { + const rootCommon = inboundState.lastCommonFromFeed?.data?.rootCommon; + + return { + ...inboundState, + lastCommonFromFeed: rootCommon + ? { + id: rootCommon.id, + data: rootCommon.data && { + ...rootCommon.data, + rootCommon: null, + }, + } + : inboundState.lastCommonFromFeed, + }; + }, + (outboundState: CommonLayoutState) => outboundState, + { whitelist: ["commonLayout"] }, +); diff --git a/yarn.lock b/yarn.lock index e4e8ed095d..a0cc55ab8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17145,6 +17145,11 @@ redux-freeze@^0.1.7: dependencies: deep-freeze-strict "1.1.1" +redux-persist@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" + integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ== + redux-saga@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-1.1.3.tgz#9f3e6aebd3c994bbc0f6901a625f9a42b51d1112"