diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 6f5ddc2663..a8c8677679 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -174,6 +174,8 @@ export default function ChatComponent({ singleEmojiText: styles.singleEmojiText, multipleEmojiText: styles.multipleEmojiText, }, + onFeedItemClick, + onUserClick, }); const { chatMessagesData, diff --git a/src/pages/common/components/ChatComponent/hooks/useDiscussionChatAdapter.ts b/src/pages/common/components/ChatComponent/hooks/useDiscussionChatAdapter.ts index ba39c257c5..2834d66445 100644 --- a/src/pages/common/components/ChatComponent/hooks/useDiscussionChatAdapter.ts +++ b/src/pages/common/components/ChatComponent/hooks/useDiscussionChatAdapter.ts @@ -6,8 +6,8 @@ import { useDiscussionMessagesById, useMarkFeedItemAsSeen, } from "@/shared/hooks/useCases"; -import { DirectParent, User } from "@/shared/models"; import { TextStyles } from "@/shared/hooks/useCases/useDiscussionMessagesById"; +import { DirectParent, User } from "@/shared/models"; interface Options { hasPermissionToHide: boolean; @@ -28,8 +28,13 @@ interface Return { } export const useDiscussionChatAdapter = (options: Options): Return => { - const { hasPermissionToHide, textStyles, discussionId } = options; - + const { + hasPermissionToHide, + textStyles, + discussionId, + onFeedItemClick, + onUserClick, + } = options; const user = useSelector(selectUser()); const userId = user?.uid; const { data: commonMembers, fetchCommonMembers } = useCommonMembers(); @@ -44,7 +49,9 @@ export const useDiscussionChatAdapter = (options: Options): Return => { discussionId, hasPermissionToHide, users, - textStyles + textStyles, + onFeedItemClick, + onUserClick, }); const { markFeedItemAsSeen } = useMarkFeedItemAsSeen(); diff --git a/src/pages/commonFeed/CommonFeed.tsx b/src/pages/commonFeed/CommonFeed.tsx index cef65399b3..f2a1354851 100644 --- a/src/pages/commonFeed/CommonFeed.tsx +++ b/src/pages/commonFeed/CommonFeed.tsx @@ -27,7 +27,11 @@ import { } from "@/shared/constants"; import { useRoutesContext } from "@/shared/contexts"; import { useAuthorizedModal, useQueryParams } from "@/shared/hooks"; -import { useCommonFeedItems, useUserCommonIds } from "@/shared/hooks/useCases"; +import { + useCommonFeedItems, + useLastVisitedCommon, + useUserCommonIds, +} from "@/shared/hooks/useCases"; import { useCommonPinnedFeedItems } from "@/shared/hooks/useCases/useCommonPinnedFeedItems"; import { SidebarIcon } from "@/shared/icons"; import { @@ -47,7 +51,6 @@ import { import { cacheActions, commonActions, - commonLayoutActions, selectCommonAction, selectRecentStreamId, selectSharedFeedItem, @@ -114,6 +117,7 @@ const CommonFeedComponent: FC = (props) => { fetched: isCommonDataFetched, fetchCommonData, } = useCommonData(userId); + const { updateLastVisitedCommon } = useLastVisitedCommon(userId); const parentCommonId = commonData?.common.directParent?.commonId; const anotherCommonId = userCommonIds[0] === commonId ? userCommonIds[1] : userCommonIds[0]; @@ -414,38 +418,28 @@ const CommonFeedComponent: FC = (props) => { }, [rootCommonMember?.id]); useEffect(() => { - return () => { + const updateLastVisited = () => { const common = stateRef.current?.data?.common; - const rootCommon = stateRef.current?.data?.rootCommon; - dispatch( - commonLayoutActions.setLastCommonFromFeed({ - id: commonId, - data: common - ? { - name: common.name, - 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, - }), - ); + updateLastVisitedCommon({ + id: commonId, + data: common + ? { + name: common.name, + image: common.image, + isProject: checkIsProject(common), + memberCount: common.memberCount, + } + : null, + }); }; - }, [commonId]); + + updateLastVisited(); + + return () => { + updateLastVisited(); + }; + }, [updateLastVisitedCommon, commonId]); if (!isDataFetched) { const headerEl = renderLoadingHeader ? ( diff --git a/src/pages/commonFeed/CommonFeedPage.tsx b/src/pages/commonFeed/CommonFeedPage.tsx index e5feeb8e05..256c09382e 100644 --- a/src/pages/commonFeed/CommonFeedPage.tsx +++ b/src/pages/commonFeed/CommonFeedPage.tsx @@ -1,12 +1,13 @@ import React, { FC, useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router-dom"; +import { selectUser } from "@/pages/Auth/store/selectors"; import { InboxItemType } from "@/shared/constants"; import { MainRoutesProvider } from "@/shared/contexts"; +import { useLastVisitedCommon } from "@/shared/hooks/useCases"; import { MultipleSpacesLayoutPageContent } from "@/shared/layouts"; import { multipleSpacesLayoutActions, - selectCommonLayoutLastCommonFromFeed, selectMultipleSpacesLayoutMainWidth, } from "@/store/states"; import BaseCommonFeedPage, { @@ -59,7 +60,9 @@ const CommonFeedPage: FC = () => { const { id: commonId } = useParams(); const dispatch = useDispatch(); const layoutMainWidth = useSelector(selectMultipleSpacesLayoutMainWidth); - const lastCommonFromFeed = useSelector(selectCommonLayoutLastCommonFromFeed); + const user = useSelector(selectUser()); + const userId = user?.uid; + const { lastVisitedCommon } = useLastVisitedCommon(userId); const onActiveItemDataChange = useActiveItemDataChange(); const feedLayoutSettings = useMemo( () => ({ @@ -68,13 +71,13 @@ const CommonFeedPage: FC = () => { }), [layoutMainWidth], ); - const lastCommonFromFeedData = lastCommonFromFeed?.data; + const lastCommonFromFeedData = lastVisitedCommon?.data; const renderLoadingHeader = lastCommonFromFeedData ? () => ( { useEffect(() => { return () => { - dispatch(multipleSpacesLayoutActions.moveBreadcrumbsToPrevious()); + dispatch(multipleSpacesLayoutActions.clearBreadcrumbs()); }; }, []); diff --git a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx index e045a84dbc..c26fe9a6e4 100644 --- a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx +++ b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx @@ -620,7 +620,13 @@ const FeedLayout: ForwardRefRenderFunction = ( }, [batchNumber]); useEffect(() => { - if (sharedFeedItemId && isTabletView && allFeedItems) { + if ( + isTabletView && + sharedFeedItemId && + allFeedItems.length > 0 && + allFeedItems.some((item) => item.itemId === sharedFeedItemId) + ) { + deleteQueryParam(QueryParamKey.Item, true); setActiveChatItem({ feedItemId: sharedFeedItemId }); } }, [sharedFeedItemId, isTabletView, allFeedItems]); diff --git a/src/services/User.ts b/src/services/User.ts index 4e30a21f4b..18c7664f31 100644 --- a/src/services/User.ts +++ b/src/services/User.ts @@ -15,7 +15,6 @@ import { convertObjectDatesToFirestoreTimestamps, convertToTimestamp, firestoreDataConverter, - transformFirebaseDataList, } from "@/shared/utils"; import firebase from "@/shared/utils/firebase"; import * as cacheActions from "@/store/states/cache/actions"; @@ -51,31 +50,56 @@ class UserService { }; }; - public getUserById = async (userId: string): Promise => { - const userSnapshot = await this.getUsersCollection() + public getUserById = async ( + userId: string, + cached = false, + ): Promise => { + const snapshot = await this.getUsersCollection() .where("uid", "==", userId) - .get(); + .get({ source: cached ? "cache" : "default" }); + const users = snapshot.docs.map((doc) => doc.data()); + const user = users[0] || null; + + if (cached && !user) { + return this.getUserById(userId); + } - return transformFirebaseDataList(userSnapshot)[0] || null; + return user; }; public getCachedUserById = async (userId: string): Promise => { - const userState = store.getState().cache.userStates[userId]; + try { + const userState = store.getState().cache.userStates[userId]; - if (userState?.fetched) { - return userState.data; - } - if (userState?.loading) { - return await waitForUserToBeLoaded(userId); - } + if (userState?.fetched) { + return userState.data; + } + if (userState?.loading) { + return await waitForUserToBeLoaded(userId); + } - store.dispatch( - cacheActions.getUserStateById.request({ - payload: { userId }, - }), - ); + store.dispatch( + cacheActions.getUserStateById.request({ + payload: { userId }, + }), + ); + + return await waitForUserToBeLoaded(userId); + } catch (err) { + const user = await this.getUserById(userId, true); + store.dispatch( + cacheActions.updateUserStateById({ + userId, + state: { + loading: false, + fetched: true, + data: user, + }, + }), + ); - return await waitForUserToBeLoaded(userId); + return user; + } }; public getCachedUsersById = async (userIds: string[]): Promise => diff --git a/src/services/UserActivity.ts b/src/services/UserActivity.ts new file mode 100644 index 0000000000..4de4e458be --- /dev/null +++ b/src/services/UserActivity.ts @@ -0,0 +1,59 @@ +import { UnsubscribeFunction } from "@/shared/interfaces"; +import { Collection, UserActivity } from "@/shared/models"; +import { firestoreDataConverter } from "@/shared/utils"; +import firebase, { isFirestoreCacheError } from "@/shared/utils/firebase"; + +const userActivityConverter = firestoreDataConverter(); + +class UserActivityService { + private getUsersActivityCollection = () => + firebase + .firestore() + .collection(Collection.UsersActivity) + .withConverter(userActivityConverter); + + public getUserActivity = async ( + userId: string, + cached = false, + ): Promise => { + try { + const snapshot = await this.getUsersActivityCollection() + .doc(userId) + .get({ source: cached ? "cache" : "default" }); + + return snapshot?.data() || null; + } catch (error) { + if (cached && isFirestoreCacheError(error)) { + return this.getUserActivity(userId); + } else { + throw error; + } + } + }; + + public updateUserActivity = async ( + userId: string, + data: Partial, + ): Promise => { + await this.getUsersActivityCollection() + .doc(userId) + .set(data, { merge: true }); + }; + + public subscribeToUserActivity = ( + userId: string, + callback: (userActivity: UserActivity) => void, + ): UnsubscribeFunction => { + const query = this.getUsersActivityCollection().doc(userId); + + return query.onSnapshot((snapshot) => { + const userActivity = snapshot.data(); + + if (userActivity) { + callback(userActivity); + } + }); + }; +} + +export default new UserActivityService(); diff --git a/src/services/index.ts b/src/services/index.ts index 4baa44fd2f..b5b8adc604 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -18,5 +18,6 @@ export { default as PayMeService } from "./PayMeService"; export { default as ProjectService } from "./Project"; export { default as ProposalService } from "./Proposal"; export { default as UserService } from "./User"; +export { default as UserActivityService } from "./UserActivity"; export { default as DiscussionMessageService } from "./DiscussionMessage"; export { default as NotionService } from "./Notion"; diff --git a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx index f3e40ca42f..ecc431e4c0 100644 --- a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx +++ b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx @@ -8,11 +8,7 @@ import React, { import classNames from "classnames"; import { useLongPress } from "use-long-press"; import { DiscussionMessageService } from "@/services"; -import { - ElementDropdown, - UserAvatar, - UserInfoPopup, -} from "@/shared/components"; +import { ElementDropdown, UserAvatar } from "@/shared/components"; import { Orientation, ChatType, @@ -145,17 +141,9 @@ export default function ChatMessage({ (discussionMessage.editedAt?.seconds ?? 0) * 1000, ); - const { - isShowing: isShowingUserProfile, - onClose: onCloseUserProfile, - onOpen: onOpenUserProfile, - } = useModal(false); - const handleUserClick = () => { if (onUserClick && discussionMessageUserId) { onUserClick(discussionMessageUserId); - } else { - onOpenUserProfile(); } }; @@ -410,16 +398,6 @@ export default function ChatMessage({ )} - {isShowingUserProfile && isUserDiscussionMessage && ( - - )} ); } diff --git a/src/shared/components/Chat/ChatMessage/DMChatMessage.tsx b/src/shared/components/Chat/ChatMessage/DMChatMessage.tsx index 0593515d22..17700d3b31 100644 --- a/src/shared/components/Chat/ChatMessage/DMChatMessage.tsx +++ b/src/shared/components/Chat/ChatMessage/DMChatMessage.tsx @@ -8,11 +8,7 @@ import React, { import classNames from "classnames"; import { useLongPress } from "use-long-press"; import { Logger } from "@/services"; -import { - ElementDropdown, - UserAvatar, - UserInfoPopup, -} from "@/shared/components"; +import { ElementDropdown, UserAvatar } from "@/shared/components"; import { Orientation, ChatType, @@ -20,7 +16,6 @@ import { QueryParamKey, } from "@/shared/constants"; import { useRoutesContext } from "@/shared/contexts"; -import { useModal } from "@/shared/hooks"; import { useIsTabletView } from "@/shared/hooks/viewport"; import { ModerationFlags } from "@/shared/interfaces/Moderation"; import { @@ -121,17 +116,9 @@ export default function DMChatMessage({ (discussionMessage.editedAt?.seconds ?? 0) * 1000, ); - const { - isShowing: isShowingUserProfile, - onClose: onCloseUserProfile, - onOpen: onOpenUserProfile, - } = useModal(false); - const handleUserClick = () => { if (onUserClick && discussionMessageUserId) { onUserClick(discussionMessageUserId); - } else { - onOpenUserProfile(); } }; @@ -468,16 +455,6 @@ export default function DMChatMessage({ )} - {isShowingUserProfile && isUserDiscussionMessage && ( - - )} ); } diff --git a/src/shared/components/Chat/ChatMessage/components/UserMention/UserMention.tsx b/src/shared/components/Chat/ChatMessage/components/UserMention/UserMention.tsx index 6246aea93d..a0d6861502 100644 --- a/src/shared/components/Chat/ChatMessage/components/UserMention/UserMention.tsx +++ b/src/shared/components/Chat/ChatMessage/components/UserMention/UserMention.tsx @@ -1,8 +1,6 @@ import React, { FC } from "react"; import classNames from "classnames"; -import { UserInfoPopup } from "@/shared/components"; -import { useModal } from "@/shared/hooks"; -import { DirectParent, User } from "@/shared/models"; +import { User } from "@/shared/models"; import { getUserName } from "@/shared/utils"; import styles from "../../ChatMessage.module.scss"; @@ -11,26 +9,13 @@ interface UserMentionProps { userId: string; displayName: string; mentionTextClassName?: string; - commonId?: string; - directParent?: DirectParent | null; onUserClick?: (userId: string) => void; } const UserMention: FC = (props) => { - const { - users, - userId, - displayName, - mentionTextClassName, - commonId, - directParent, - onUserClick, - } = props; - const { - isShowing: isShowingUserProfile, - onClose: onCloseUserProfile, - onOpen: onOpenUserProfile, - } = useModal(false); + const { users, userId, displayName, mentionTextClassName, onUserClick } = + props; + const user = users.find(({ uid }) => uid === userId); const withSpace = displayName[displayName.length - 1] === " "; const userName = user @@ -40,8 +25,6 @@ const UserMention: FC = (props) => { const handleUserNameClick = () => { if (onUserClick) { onUserClick(userId); - } else { - onOpenUserProfile(); } }; @@ -53,16 +36,6 @@ const UserMention: FC = (props) => { > @{userName} - {!onUserClick && ( - - )} ); }; diff --git a/src/shared/components/Chat/ChatMessage/utils/getTextFromSystemMessage.tsx b/src/shared/components/Chat/ChatMessage/utils/getTextFromSystemMessage.tsx index 7267d13c75..8a8005b47d 100644 --- a/src/shared/components/Chat/ChatMessage/utils/getTextFromSystemMessage.tsx +++ b/src/shared/components/Chat/ChatMessage/utils/getTextFromSystemMessage.tsx @@ -42,8 +42,6 @@ const renderUserMention = ( userId={user.uid} displayName={getUserName(user)} mentionTextClassName={data.mentionTextClassName} - commonId={data.commonId} - directParent={data.directParent} onUserClick={data.onUserClick} /> ) : ( diff --git a/src/shared/components/Chat/ChatMessage/utils/getTextFromTextEditorString.tsx b/src/shared/components/Chat/ChatMessage/utils/getTextFromTextEditorString.tsx index 622c859ca0..9d063f6719 100644 --- a/src/shared/components/Chat/ChatMessage/utils/getTextFromTextEditorString.tsx +++ b/src/shared/components/Chat/ChatMessage/utils/getTextFromTextEditorString.tsx @@ -72,8 +72,6 @@ const getTextFromDescendant = ({ userId={descendant.userId} displayName={descendant.displayName} mentionTextClassName={mentionTextClassName} - commonId={commonId} - directParent={directParent} onUserClick={onUserClick} /> ); diff --git a/src/shared/hooks/useCases/index.ts b/src/shared/hooks/useCases/index.ts index 51b09e439f..1fda0b1ab5 100644 --- a/src/shared/hooks/useCases/index.ts +++ b/src/shared/hooks/useCases/index.ts @@ -23,6 +23,7 @@ export { useProposalById } from "./useProposalById"; export { useRootCommonMembershipIntro } from "./useRootCommonMembershipIntro"; export { useSubCommons } from "./useSubCommons"; export { useSupportersData } from "./useSupportersData"; +export { useLastVisitedCommon } from "./useLastVisitedCommon"; export { useUserById } from "./useUserById"; export { default as useUserCards } from "./useUserCards"; export { useUserCommonIds } from "./useUserCommonIds"; diff --git a/src/shared/hooks/useCases/useInboxItems.ts b/src/shared/hooks/useCases/useInboxItems.ts index 5966ae3f4d..e52ed2c95a 100644 --- a/src/shared/hooks/useCases/useInboxItems.ts +++ b/src/shared/hooks/useCases/useInboxItems.ts @@ -13,6 +13,7 @@ import { useIsMounted } from "@/shared/hooks"; import { FeedLayoutItemWithFollowData } from "@/shared/interfaces"; import { ChatChannel, + CommonFeedType, FeedItemFollow, FeedItemFollowWithMetadata, } from "@/shared/models"; @@ -189,13 +190,16 @@ export const useInboxItems = ( return; } + const filteredData = data.filter(({ item }) => + [CommonFeedType.Discussion, CommonFeedType.Proposal].includes(item.type), + ); const finalData = feedItemIdsForNotListening && feedItemIdsForNotListening.length > 0 - ? data.filter( + ? filteredData.filter( (item) => !feedItemIdsForNotListening.includes(item.item.feedItemId), ) - : data; + : filteredData; setNewItemsBatches((currentItems) => [...currentItems, finalData]); }; diff --git a/src/shared/hooks/useCases/useLastVisitedCommon.ts b/src/shared/hooks/useCases/useLastVisitedCommon.ts new file mode 100644 index 0000000000..3fa8672057 --- /dev/null +++ b/src/shared/hooks/useCases/useLastVisitedCommon.ts @@ -0,0 +1,68 @@ +import { useCallback, useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { Logger, UserActivityService } from "@/services"; +import { + commonLayoutActions, + CommonLayoutState, + selectCommonLayoutLastCommonFromFeed, +} from "@/store/states"; + +interface Return { + lastVisitedCommon: CommonLayoutState["lastCommonFromFeed"]; + updateLastVisitedCommon: ( + data: CommonLayoutState["lastCommonFromFeed"], + ) => void; +} + +export const useLastVisitedCommon = (userId?: string): Return => { + const dispatch = useDispatch(); + const lastVisitedCommon = useSelector(selectCommonLayoutLastCommonFromFeed); + + const updateLastVisitedCommon = useCallback< + Return["updateLastVisitedCommon"] + >( + async (data) => { + dispatch(commonLayoutActions.setLastCommonFromFeed(data)); + + if (!userId || !data?.id) { + return; + } + + try { + await UserActivityService.updateUserActivity(userId, { + lastVisitedCommon: data.id, + }); + } catch (error) { + Logger.error(error); + } + }, + [userId], + ); + + useEffect(() => { + if (!userId) { + return; + } + + const unsubscribe = UserActivityService.subscribeToUserActivity( + userId, + (updatedUserActivity) => { + if (updatedUserActivity.lastVisitedCommon) { + dispatch( + commonLayoutActions.setLastCommonFromFeed({ + id: updatedUserActivity.lastVisitedCommon, + data: null, + }), + ); + } + }, + ); + + return unsubscribe; + }, [userId]); + + return { + lastVisitedCommon, + updateLastVisitedCommon, + }; +}; diff --git a/src/shared/icons/auth/apple.icon.tsx b/src/shared/icons/auth/apple.icon.tsx index fff3aeb538..1344f4b37e 100644 --- a/src/shared/icons/auth/apple.icon.tsx +++ b/src/shared/icons/auth/apple.icon.tsx @@ -12,8 +12,8 @@ const AppleIcon: FC = () => { xmlns="http://www.w3.org/2000/svg" > diff --git a/src/shared/icons/auth/google.icon.tsx b/src/shared/icons/auth/google.icon.tsx index 2fd35cb30c..33719d667c 100644 --- a/src/shared/icons/auth/google.icon.tsx +++ b/src/shared/icons/auth/google.icon.tsx @@ -12,26 +12,26 @@ const GoogleIcon: FC = () => { xmlns="http://www.w3.org/2000/svg" > diff --git a/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/LayoutTabs.tsx b/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/LayoutTabs.tsx index d698b0fb55..de37d17d22 100644 --- a/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/LayoutTabs.tsx +++ b/src/shared/layouts/CommonSidenavLayout/components/LayoutTabs/LayoutTabs.tsx @@ -4,15 +4,18 @@ import { useHistory } from "react-router-dom"; import classNames from "classnames"; import { authentificated, + selectUser, selectUserStreamsWithNotificationsAmount, } from "@/pages/Auth/store/selectors"; import { Tab, Tabs } from "@/shared/components"; import { useRoutesContext } from "@/shared/contexts"; import { useModal } from "@/shared/hooks"; -import { useUserCommonIds } from "@/shared/hooks/useCases"; +import { + useLastVisitedCommon, + 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"; import styles from "./LayoutTabs.module.scss"; @@ -34,14 +37,14 @@ const LayoutTabs: FC = (props) => { const history = useHistory(); const { getCommonPagePath, getInboxPagePath, getProfilePagePath } = useRoutesContext(); - const lastCommonIdFromFeed = useSelector( - selectCommonLayoutLastCommonFromFeed, - ); const isAuthenticated = useSelector(authentificated()); const userStreamsWithNotificationsAmount = useSelector( selectUserStreamsWithNotificationsAmount(), ); + const user = useSelector(selectUser()); + const userId = user?.uid; const { data: userCommonIds } = useUserCommonIds(); + const { lastVisitedCommon } = useLastVisitedCommon(userId); const { isShowing: isCreateCommonPromptOpen, onOpen: onCreateCommonPromptOpen, @@ -81,7 +84,7 @@ const LayoutTabs: FC = (props) => { } as CSSProperties; const handleSpacesClick = () => { - const commonForRedirectId = lastCommonIdFromFeed?.id || userCommonIds[0]; + const commonForRedirectId = lastVisitedCommon?.id || userCommonIds[0]; if (commonForRedirectId) { history.push(getCommonPagePath(commonForRedirectId)); 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 dd86ccc171..f86df49344 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,5 +1,7 @@ -import React, { FC } from "react"; +import React, { FC, useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { CommonEvent, CommonEventEmitter } from "@/events"; +import { CommonService } from "@/services"; import { commonLayoutActions, MultipleSpacesLayoutFeedItemBreadcrumbs, @@ -7,7 +9,6 @@ import { selectCommonLayoutCommonId, } from "@/store/states"; import { useGoToCreateCommon } from "../../../../../../hooks"; -import { LoadingBreadcrumbsItem } from "../LoadingBreadcrumbsItem"; import { Separator } from "../Separator"; import { ActiveFeedBreadcrumbsItem, FeedBreadcrumbsItem } from "./components"; import styles from "./FeedItemBreadcrumbs.module.scss"; @@ -34,9 +35,30 @@ const FeedItemBreadcrumbs: FC = (props) => { } }; + useEffect(() => { + const commonIds = breadcrumbs.items.map((item) => item.commonId); + + if (commonIds.length === 0) { + return; + } + + const unsubscribe = CommonService.subscribeToCommons(commonIds, (data) => { + data.forEach(({ common }) => { + CommonEventEmitter.emit(CommonEvent.ProjectUpdated, { + commonId: common.id, + image: common.image, + name: common.name, + directParent: common.directParent, + rootCommonId: common.rootCommonId, + }); + }); + }); + + return unsubscribe; + }, [breadcrumbs.activeItem?.id]); + return (
    - {breadcrumbs.areItemsLoading && } {!breadcrumbs.areItemsLoading && breadcrumbs.items.map((item, index) => ( @@ -51,7 +73,7 @@ const FeedItemBreadcrumbs: FC = (props) => { ))} {breadcrumbs.activeItem && ( <> - {(breadcrumbs.areItemsLoading || breadcrumbs.items.length > 0) && ( + {!breadcrumbs.areItemsLoading && breadcrumbs.items.length > 0 && ( )} = (props) => { const userStreamsWithNotificationsAmount = useSelector( selectUserStreamsWithNotificationsAmount(), ); - const currentBreadcrumbs = useSelector(selectMultipleSpacesLayoutBreadcrumbs); - const previousBreadcrumbs = useSelector( - selectMultipleSpacesLayoutPreviousBreadcrumbs, - ); + const user = useSelector(selectUser()); + const userId = user?.uid; const { data: userCommonIds } = useUserCommonIds(); - const breadcrumbs = previousBreadcrumbs || currentBreadcrumbs; - const breadcrumbsCommonId = - breadcrumbs?.type === InboxItemType.FeedItemFollow - ? breadcrumbs.activeCommonId - : ""; - const mySpacesCommonId = breadcrumbsCommonId || userCommonIds[0] || ""; + const { lastVisitedCommon } = useLastVisitedCommon(userId); + const mySpacesCommonId = lastVisitedCommon?.id || userCommonIds[0] || ""; const mySpacesPagePath = ( mySpacesCommonId ? getCommonPagePath(mySpacesCommonId) : "" ) as ROUTE_PATHS; diff --git a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/Scrollbar/Scrollbar.module.scss b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/Scrollbar/Scrollbar.module.scss index 0c33878853..52901cf672 100644 --- a/src/shared/layouts/SidenavLayout/components/SidenavContent/components/Scrollbar/Scrollbar.module.scss +++ b/src/shared/layouts/SidenavLayout/components/SidenavContent/components/Scrollbar/Scrollbar.module.scss @@ -34,7 +34,9 @@ &:active { --scroll-thumb-color: #{$c-neutrals-300}; } - &:hover { - --scroll-thumb-color: $c-gray-20; + @media (hover: hover) and (pointer: fine) { + &:hover { + --scroll-thumb-color: $c-gray-20; + } } } diff --git a/src/shared/models/UserActivity.ts b/src/shared/models/UserActivity.ts new file mode 100644 index 0000000000..3451afaa49 --- /dev/null +++ b/src/shared/models/UserActivity.ts @@ -0,0 +1,3 @@ +export interface UserActivity { + lastVisitedCommon?: string; +} diff --git a/src/shared/models/index.tsx b/src/shared/models/index.tsx index d1c61c51e4..fbb74b0468 100644 --- a/src/shared/models/index.tsx +++ b/src/shared/models/index.tsx @@ -24,4 +24,5 @@ export * from "./BankAccountDetails"; export * from "./Currency"; export * from "./SupportersData"; export * from "./Timestamp"; +export * from "./UserActivity"; export * from "./NotionIntegration"; diff --git a/src/shared/models/shared.tsx b/src/shared/models/shared.tsx index 8ff4b09431..60980a54d1 100644 --- a/src/shared/models/shared.tsx +++ b/src/shared/models/shared.tsx @@ -33,6 +33,7 @@ export enum Collection { Supporters = "supporters", Notifications = "notification", BankAccountDetails = "bankAccountDetails", + UsersActivity = "usersActivity", } export enum SubCollections { diff --git a/src/store/states/cache/index.ts b/src/store/states/cache/index.ts index 2a0fca605d..ea30e2f27e 100644 --- a/src/store/states/cache/index.ts +++ b/src/store/states/cache/index.ts @@ -1,5 +1,5 @@ export * as cacheActions from "./actions"; -export { reducer as cacheReducer } from "./reducer"; +export { reducer as cacheReducer, INITIAL_CACHE_STATE } from "./reducer"; export { mainSaga as cacheSaga } from "./saga"; export * from "./selectors"; export * from "./types"; diff --git a/src/store/states/cache/reducer.tsx b/src/store/states/cache/reducer.tsx index d30e54de54..50874f2638 100644 --- a/src/store/states/cache/reducer.tsx +++ b/src/store/states/cache/reducer.tsx @@ -8,7 +8,7 @@ import { CacheState } from "./types"; type Action = ActionType; -const initialState: CacheState = { +export const INITIAL_CACHE_STATE: CacheState = { userStates: {}, governanceByCommonIdStates: {}, discussionStates: {}, @@ -19,7 +19,7 @@ const initialState: CacheState = { chatChannelUserStatusStates: {}, }; -export const reducer = createReducer(initialState) +export const reducer = createReducer(INITIAL_CACHE_STATE) .handleAction(actions.updateUserStateById, (state, { payload }) => produce(state, (nextState) => { const { userId, state } = payload; @@ -115,8 +115,7 @@ export const reducer = createReducer(initialState) const uniq = unionBy( payload.state?.data ?? [], - state.discussionMessagesStates[discussionId]?.data ?? - [], + state.discussionMessagesStates[discussionId]?.data ?? [], "id", ).sort( (a, b) => @@ -136,8 +135,7 @@ export const reducer = createReducer(initialState) const { discussionId, discussionMessage } = payload; const updatedDiscussionMessages = [ - ...(state.discussionMessagesStates[discussionId] - ?.data ?? []), + ...(state.discussionMessagesStates[discussionId]?.data ?? []), discussionMessage, ]; diff --git a/src/store/states/commonLayout/saga/getCommons.ts b/src/store/states/commonLayout/saga/getCommons.ts index 72ed1e171b..78b2956aac 100644 --- a/src/store/states/commonLayout/saga/getCommons.ts +++ b/src/store/states/commonLayout/saga/getCommons.ts @@ -1,6 +1,11 @@ import { call, put, select } from "redux-saga/effects"; import { selectUser } from "@/pages/Auth/store/selectors"; -import { CommonService, GovernanceService, ProjectService } from "@/services"; +import { + CommonService, + GovernanceService, + ProjectService, + UserActivityService, +} from "@/services"; import { Awaited } from "@/shared/interfaces"; import { User } from "@/shared/models"; import { compareCommonsByLastActivity, isError } from "@/shared/utils"; @@ -70,11 +75,20 @@ const getProjectsInfo = async ( export function* getCommons( action: ReturnType, ) { - const { payload: commonId = "" } = action; + let { payload: commonId = "" } = action; try { const user = (yield select(selectUser())) as User | null; const userId = user?.uid; + + if (!commonId && userId) { + const userActivity = (yield call( + UserActivityService.getUserActivity, + userId, + )) as Awaited>; + commonId = userActivity?.lastVisitedCommon || ""; + } + const { data, currentCommonId } = (yield call( getProjectsInfo, commonId, diff --git a/src/store/states/commonLayout/types.ts b/src/store/states/commonLayout/types.ts index fbe4fc9664..7e16b8515a 100644 --- a/src/store/states/commonLayout/types.ts +++ b/src/store/states/commonLayout/types.ts @@ -1,22 +1,13 @@ 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; + data: { + name: string; + image: string; + isProject: boolean; + memberCount: number; + } | null; } export interface CommonLayoutState { diff --git a/src/store/states/multipleSpacesLayout/actions.ts b/src/store/states/multipleSpacesLayout/actions.ts index cb8210e550..251514f6d0 100644 --- a/src/store/states/multipleSpacesLayout/actions.ts +++ b/src/store/states/multipleSpacesLayout/actions.ts @@ -36,8 +36,8 @@ export const setBreadcrumbsData = createStandardAction( MultipleSpacesLayoutActionType.SET_BREADCRUMBS_DATA, )(); -export const moveBreadcrumbsToPrevious = createStandardAction( - MultipleSpacesLayoutActionType.MOVE_BREADCRUMBS_TO_PREVIOUS, +export const clearBreadcrumbs = createStandardAction( + MultipleSpacesLayoutActionType.CLEAR_BREADCRUMBS, )(); export const addOrUpdateProjectInBreadcrumbs = createStandardAction( diff --git a/src/store/states/multipleSpacesLayout/constants.ts b/src/store/states/multipleSpacesLayout/constants.ts index 7c5574dedb..6811fa17ad 100644 --- a/src/store/states/multipleSpacesLayout/constants.ts +++ b/src/store/states/multipleSpacesLayout/constants.ts @@ -10,7 +10,7 @@ export enum MultipleSpacesLayoutActionType { SET_BREADCRUMBS_DATA = "@MULTIPLE_SPACES_LAYOUT/SET_BREADCRUMBS_DATA", - MOVE_BREADCRUMBS_TO_PREVIOUS = "@MULTIPLE_SPACES_LAYOUT/MOVE_BREADCRUMBS_TO_PREVIOUS", + CLEAR_BREADCRUMBS = "@MULTIPLE_SPACES_LAYOUT/CLEAR_BREADCRUMBS", ADD_OR_UPDATE_PROJECT_IN_BREADCRUMBS = "@MULTIPLE_SPACES_LAYOUT/ADD_OR_UPDATE_PROJECT_IN_BREADCRUMBS", diff --git a/src/store/states/multipleSpacesLayout/reducer.ts b/src/store/states/multipleSpacesLayout/reducer.ts index 25fd6537ec..a05f3f182e 100644 --- a/src/store/states/multipleSpacesLayout/reducer.ts +++ b/src/store/states/multipleSpacesLayout/reducer.ts @@ -10,7 +10,6 @@ type Action = ActionType; const initialState: MultipleSpacesLayoutState = { breadcrumbs: null, - previousBreadcrumbs: null, backUrl: null, mainWidth: window.innerWidth, }; @@ -49,11 +48,8 @@ export const reducer = createReducer( nextState.breadcrumbs = payload && { ...payload }; }), ) - .handleAction(actions.moveBreadcrumbsToPrevious, (state) => + .handleAction(actions.clearBreadcrumbs, (state) => produce(state, (nextState) => { - nextState.previousBreadcrumbs = nextState.breadcrumbs && { - ...nextState.breadcrumbs, - }; nextState.breadcrumbs = null; }), ) diff --git a/src/store/states/multipleSpacesLayout/saga/configureBreadcrumbsData.ts b/src/store/states/multipleSpacesLayout/saga/configureBreadcrumbsData.ts index 9a189226e1..c877af75e2 100644 --- a/src/store/states/multipleSpacesLayout/saga/configureBreadcrumbsData.ts +++ b/src/store/states/multipleSpacesLayout/saga/configureBreadcrumbsData.ts @@ -1,6 +1,7 @@ import { put, select } from "redux-saga/effects"; import { InboxItemType } from "@/shared/constants"; import { + MultipleSpacesLayoutFeedItemBreadcrumbs, selectCommonLayoutCommonsState, selectCommonLayoutProjectsState, } from "@/store/states"; @@ -39,6 +40,42 @@ const getItemsByExistingData = ( return items; }; +const getNextBreadcrumbsData = ( + items: ProjectsStateItem[] | null, + currentBreadcrumbs: MultipleSpacesLayoutFeedItemBreadcrumbs | null, + activeCommonId: string, +): Pick< + MultipleSpacesLayoutFeedItemBreadcrumbs, + "items" | "areItemsLoading" | "areItemsFetched" +> => { + if (items) { + return { + items, + areItemsLoading: false, + areItemsFetched: true, + }; + } + if (!currentBreadcrumbs) { + return { + items: [], + areItemsLoading: true, + areItemsFetched: false, + }; + } + + const activeItemIndex = currentBreadcrumbs.items.findIndex( + (item) => item.commonId === activeCommonId, + ); + + return { + ...currentBreadcrumbs, + items: + activeItemIndex > -1 + ? currentBreadcrumbs.items.slice(0, activeItemIndex + 1) + : currentBreadcrumbs.items, + }; +}; + export function* configureBreadcrumbsData( action: ReturnType, ) { @@ -88,17 +125,11 @@ export function* configureBreadcrumbsData( yield put( actions.setBreadcrumbsData({ - ...(items - ? { - items, - areItemsLoading: false, - areItemsFetched: true, - } - : currentBreadcrumbs || { - items: [], - areItemsLoading: true, - areItemsFetched: false, - }), + ...getNextBreadcrumbsData( + items, + currentBreadcrumbs, + payload.activeCommonId, + ), type: InboxItemType.FeedItemFollow, activeItem: payload.activeItem ? { ...payload.activeItem } : null, activeCommonId: payload.activeCommonId, diff --git a/src/store/states/multipleSpacesLayout/saga/fetchBreadcrumbsItemsByCommonId.ts b/src/store/states/multipleSpacesLayout/saga/fetchBreadcrumbsItemsByCommonId.ts index c7e27de108..b99dbf8220 100644 --- a/src/store/states/multipleSpacesLayout/saga/fetchBreadcrumbsItemsByCommonId.ts +++ b/src/store/states/multipleSpacesLayout/saga/fetchBreadcrumbsItemsByCommonId.ts @@ -9,7 +9,7 @@ import { MultipleSpacesLayoutState, ProjectsStateItem } from "../types"; const fetchProjectsInfoByActiveCommonId = async ( commonId: string, ): Promise => { - const activeCommon = await CommonService.getCommonById(commonId); + const activeCommon = await CommonService.getCommonById(commonId, true); if (!activeCommon) { return []; @@ -17,6 +17,7 @@ const fetchProjectsInfoByActiveCommonId = async ( const commons = await CommonService.getAllParentCommonsForCommon( activeCommon, + true, ); return [...commons, activeCommon].map((common) => ({ diff --git a/src/store/states/multipleSpacesLayout/selectors.ts b/src/store/states/multipleSpacesLayout/selectors.ts index fff25a04d6..6c5600bd0a 100644 --- a/src/store/states/multipleSpacesLayout/selectors.ts +++ b/src/store/states/multipleSpacesLayout/selectors.ts @@ -3,10 +3,6 @@ import { AppState } from "@/shared/interfaces"; export const selectMultipleSpacesLayoutBreadcrumbs = (state: AppState) => state.multipleSpacesLayout.breadcrumbs; -export const selectMultipleSpacesLayoutPreviousBreadcrumbs = ( - state: AppState, -) => state.multipleSpacesLayout.previousBreadcrumbs; - export const selectMultipleSpacesLayoutBackUrl = (state: AppState) => state.multipleSpacesLayout.backUrl; diff --git a/src/store/states/multipleSpacesLayout/types.ts b/src/store/states/multipleSpacesLayout/types.ts index 3e2976f09f..8ba7a3534d 100644 --- a/src/store/states/multipleSpacesLayout/types.ts +++ b/src/store/states/multipleSpacesLayout/types.ts @@ -27,7 +27,6 @@ export type MultipleSpacesLayoutBreadcrumbs = export interface MultipleSpacesLayoutState { breadcrumbs: MultipleSpacesLayoutBreadcrumbs | null; - previousBreadcrumbs: MultipleSpacesLayoutBreadcrumbs | null; backUrl: string | null; mainWidth: number; } diff --git a/src/store/store.tsx b/src/store/store.tsx index f62f433117..b56c38a915 100644 --- a/src/store/store.tsx +++ b/src/store/store.tsx @@ -20,8 +20,8 @@ import rootReducer from "./reducer"; import appSagas from "./saga"; import { inboxTransform, - lastCommonFromFeedTransform, cacheTransform, + multipleSpacesLayoutTransform, } from "./transforms"; const persistConfig: PersistConfig = { @@ -32,12 +32,11 @@ const persistConfig: PersistConfig = { "commonLayout", "commonFeedFollows", "cache", - "chat", "inbox", "multipleSpacesLayout", ], stateReconciler: autoMergeLevel2, - transforms: [inboxTransform, lastCommonFromFeedTransform, cacheTransform], + transforms: [inboxTransform, cacheTransform, multipleSpacesLayoutTransform], }; const sagaMiddleware = createSagaMiddleware(); diff --git a/src/store/transforms.ts b/src/store/transforms.ts index 625133dfc4..b8622cbe89 100644 --- a/src/store/transforms.ts +++ b/src/store/transforms.ts @@ -1,9 +1,12 @@ import { createTransform } from "redux-persist"; -import { deserializeFeedLayoutItemWithFollowData } from "@/shared/interfaces"; +import { + deserializeFeedLayoutItemWithFollowData, + LoadingState, +} from "@/shared/interfaces"; import { convertObjectDatesToFirestoreTimestamps } from "@/shared/utils"; +import { MultipleSpacesLayoutState } from "@/store/states"; import { getFeedLayoutItemDateForSorting } from "@/store/states/inbox/utils"; -import { CommonLayoutState } from "./states/commonLayout"; -import { CacheState } from "./states/cache"; +import { CacheState, INITIAL_CACHE_STATE } from "./states/cache"; import { InboxItems, InboxState, @@ -11,6 +14,20 @@ import { INITIAL_INBOX_STATE, } from "./states/inbox"; +const clearNonFinishedStates = ( + states: Record>, +): Record> => + Object.entries(states).reduce((acc, [key, value]) => { + if (value.loading || !value.fetched) { + return acc; + } + + return { + ...acc, + [key]: value, + }; + }, {}); + export const inboxTransform = createTransform( (inboundState: InboxState) => { if (inboundState.items.unread) { @@ -64,32 +81,22 @@ export const inboxTransform = createTransform( { 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"] }, -); - export const cacheTransform = createTransform( - (inboundState: CacheState) => inboundState, - (outboundState: CacheState) => ({ - ...outboundState, - discussionMessagesStates: {} + (inboundState: CacheState) => ({ + ...INITIAL_CACHE_STATE, + userStates: clearNonFinishedStates(inboundState.userStates), + feedByCommonIdStates: inboundState.feedByCommonIdStates, }), + (outboundState: CacheState) => outboundState, { whitelist: ["cache"] }, ); + +export const multipleSpacesLayoutTransform = createTransform( + (inboundState: MultipleSpacesLayoutState) => ({ + ...inboundState, + breadcrumbs: null, + backUrl: null, + }), + (outboundState: MultipleSpacesLayoutState) => outboundState, + { whitelist: ["multipleSpacesLayout"] }, +);