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"