diff --git a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx index cc7951603e..3ba0ea685d 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -135,6 +135,7 @@ const DiscussionFeedCard = forwardRef( commonMember, feedItemFollow, getNonAllowedItems, + feedItemUserMetadata, }, { report: onReportModalOpen, @@ -324,6 +325,7 @@ const DiscussionFeedCard = forwardRef( isLoading={isLoading} menuItems={menuItems} seenOnce={feedItemUserMetadata?.seenOnce} + seen={feedItemUserMetadata?.seen} ownerId={item.userId} discussionPredefinedType={discussion?.predefinedType} > diff --git a/src/pages/common/components/DiscussionFeedCard/hooks/useMenuItems.tsx b/src/pages/common/components/DiscussionFeedCard/hooks/useMenuItems.tsx index 8a45faa153..1ce6e71779 100644 --- a/src/pages/common/components/DiscussionFeedCard/hooks/useMenuItems.tsx +++ b/src/pages/common/components/DiscussionFeedCard/hooks/useMenuItems.tsx @@ -12,6 +12,7 @@ import { Trash2Icon, UnfollowIcon, UnpinIcon, + Message3Icon, } from "@/shared/icons"; import { ContextMenuItem as Item, UploadFile } from "@/shared/interfaces"; import { parseStringToTextEditorValue } from "@/shared/ui-kit"; @@ -31,7 +32,13 @@ export const useMenuItems = ( actions: Actions, ): Item[] => { const dispatch = useDispatch(); - const { discussion, commonId, feedItem, feedItemFollow } = options; + const { + discussion, + commonId, + feedItem, + feedItemFollow, + feedItemUserMetadata, + } = options; const { report, share, remove } = actions; const allowedMenuItems = getAllowedItems({ ...options, feedItemFollow }); const items: Item[] = [ @@ -59,6 +66,38 @@ export const useMenuItems = ( onClick: share, icon: , }, + { + id: FeedItemMenuItem.MarkUnread, + text: "Mark as unread", + onClick: async () => { + if (!commonId || !feedItem) { + return; + } + + await CommonFeedService.markCommonFeedItemAsUnseen( + commonId, + feedItem.id, + ); + }, + icon: , + }, + { + id: FeedItemMenuItem.MarkRead, + text: "Mark as read", + onClick: async () => { + if (!commonId || !feedItem) { + return; + } + + await CommonFeedService.markCommonFeedItemAsSeen({ + commonId, + feedObjectId: feedItem.id, + lastSeenId: feedItemUserMetadata?.lastSeen?.id, + type: feedItemUserMetadata?.lastSeen?.type, + }); + }, + icon: , + }, { id: FeedItemMenuItem.Report, text: "Report", diff --git a/src/pages/common/components/DiscussionFeedCard/utils/getAllowedItems.ts b/src/pages/common/components/DiscussionFeedCard/utils/getAllowedItems.ts index c220fb84a3..c253616b6c 100644 --- a/src/pages/common/components/DiscussionFeedCard/utils/getAllowedItems.ts +++ b/src/pages/common/components/DiscussionFeedCard/utils/getAllowedItems.ts @@ -1,4 +1,5 @@ import { CommonFeedType } from "@/shared/models"; +import { notEmpty } from "@/shared/utils/notEmpty"; import { FeedItemMenuItem, FeedItemPinAction } from "../../FeedItem/constants"; import { GetAllowedItemsOptions } from "../../FeedItem/types"; import { checkIsEditItemAllowed } from "./checkIsEditItemAllowed"; @@ -27,6 +28,16 @@ const MENU_ITEM_TO_CHECK_FUNCTION_MAP: Record< !options.feedItemFollow.isDisabled && options.feedItemFollow.isFollowing ); }, + [FeedItemMenuItem.MarkUnread]: ({ feedItemUserMetadata }) => { + const { count, seen } = feedItemUserMetadata || {}; + + return notEmpty(count) && notEmpty(seen) && count === 0 && seen; + }, + [FeedItemMenuItem.MarkRead]: ({ feedItemUserMetadata }) => { + const { count, seen } = feedItemUserMetadata || {}; + + return Boolean(count) || !seen; + }, }; export const getAllowedItems = ( @@ -38,6 +49,8 @@ export const getAllowedItems = ( FeedItemMenuItem.Pin, FeedItemMenuItem.Unpin, FeedItemMenuItem.Share, + FeedItemMenuItem.MarkUnread, + FeedItemMenuItem.MarkRead, FeedItemMenuItem.Report, FeedItemMenuItem.Edit, FeedItemMenuItem.Remove, diff --git a/src/pages/common/components/FeedCard/FeedCard.tsx b/src/pages/common/components/FeedCard/FeedCard.tsx index ea660ca610..d1ea7ab55a 100644 --- a/src/pages/common/components/FeedCard/FeedCard.tsx +++ b/src/pages/common/components/FeedCard/FeedCard.tsx @@ -40,6 +40,7 @@ type FeedCardProps = PropsWithChildren<{ type?: CommonFeedType; menuItems?: ContextMenuItem[]; seenOnce?: boolean; + seen?: boolean; ownerId?: string; discussionPredefinedType?: PredefinedTypes; hasFiles?: boolean; @@ -77,6 +78,7 @@ export const FeedCard = forwardRef((props, ref) => { type, menuItems, seenOnce, + seen, ownerId, discussionPredefinedType, hasImages, @@ -197,6 +199,7 @@ export const FeedCard = forwardRef((props, ref) => { isFollowing, type, seenOnce, + seen, ownerId, discussionPredefinedType, hasFiles, diff --git a/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.module.scss b/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.module.scss index 41c3f0fd31..0f0985d0b5 100644 --- a/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.module.scss +++ b/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.module.scss @@ -50,3 +50,9 @@ margin-left: 0.75rem; color: $c-gray-800; } + +.unseen { + width: 1.4375rem; + border-radius: 50%; + background-color: $c-pink-primary; +} diff --git a/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx b/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx index 03868c5aa5..ca794ad258 100644 --- a/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx +++ b/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx @@ -4,12 +4,14 @@ import classNames from "classnames"; import { selectUser } from "@/pages/Auth/store/selectors"; import { PinIcon, StarIcon } from "@/shared/icons"; import { CommonFeedType } from "@/shared/models"; +import { notEmpty } from "@/shared/utils/notEmpty"; import styles from "./FeedCardTags.module.scss"; interface FeedCardTagsProps { unreadMessages?: number; type?: CommonFeedType; seenOnce?: boolean; + seen?: boolean; ownerId?: string; isActive: boolean; isPinned?: boolean; @@ -21,6 +23,7 @@ export const FeedCardTags: FC = (props) => { unreadMessages, type, seenOnce, + seen, ownerId, isActive, isPinned, @@ -69,6 +72,9 @@ export const FeedCardTags: FC = (props) => { {unreadMessages} )} + {!unreadMessages && notEmpty(seen) && !seen && ( +
+ )} ); }; diff --git a/src/pages/common/components/FeedCard/components/FeedItemBaseContent/FeedItemBaseContent.tsx b/src/pages/common/components/FeedCard/components/FeedItemBaseContent/FeedItemBaseContent.tsx index a2e8bd22a3..f51be70090 100644 --- a/src/pages/common/components/FeedCard/components/FeedItemBaseContent/FeedItemBaseContent.tsx +++ b/src/pages/common/components/FeedCard/components/FeedItemBaseContent/FeedItemBaseContent.tsx @@ -1,7 +1,6 @@ import React, { FC, MouseEventHandler, useRef, useState } from "react"; import classNames from "classnames"; import { useLongPress } from "use-long-press"; -import { PredefinedTypes } from "@/shared/models"; import { checkIsTextEditorValueEmpty, ContextMenu, @@ -29,6 +28,7 @@ export const FeedItemBaseContent: FC = (props) => { type, menuItems, seenOnce, + seen, ownerId, renderLeftContent, isPinned, @@ -149,6 +149,7 @@ export const FeedItemBaseContent: FC = (props) => { unreadMessages={unreadMessages} type={type} seenOnce={seenOnce} + seen={seen} ownerId={ownerId} isActive={isActive} isPinned={isPinned} diff --git a/src/pages/common/components/FeedItem/FeedItem.tsx b/src/pages/common/components/FeedItem/FeedItem.tsx index 96b79f19fb..bafa9ab5eb 100644 --- a/src/pages/common/components/FeedItem/FeedItem.tsx +++ b/src/pages/common/components/FeedItem/FeedItem.tsx @@ -15,6 +15,7 @@ import { DiscussionFeedCard } from "../DiscussionFeedCard"; import { ProposalFeedCard } from "../ProposalFeedCard"; import { ProjectFeedItem } from "./components"; import { useFeedItemContext } from "./context"; +import { useFeedItemCounters } from "./hooks"; import { FeedItemRef } from "./types"; interface FeedItemProps { @@ -67,6 +68,8 @@ const FeedItem = forwardRef((props, ref) => { const { onFeedItemUpdate, getLastMessage, getNonAllowedItems, onUserSelect } = useFeedItemContext(); useFeedItemSubscription(item.id, commonId, onFeedItemUpdate); + const { projectUnreadStreamsCount, projectUnreadMessages } = + useFeedItemCounters(item.id, commonId); if ( shouldCheckItemVisibility && @@ -115,7 +118,14 @@ const FeedItem = forwardRef((props, ref) => { } if (item.data.type === CommonFeedType.Project) { - return ; + return ( + + ); } return null; diff --git a/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx b/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx index 7c948ed356..fdb107b579 100644 --- a/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx +++ b/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx @@ -8,24 +8,21 @@ import { OpenIcon } from "@/shared/icons"; import { CommonFeed } from "@/shared/models"; import { CommonAvatar, parseStringToTextEditorValue } from "@/shared/ui-kit"; import { checkIsProject } from "@/shared/utils"; -import { useFeedItemCounters } from "../../hooks"; import styles from "./ProjectFeedItem.module.scss"; interface ProjectFeedItemProps { item: CommonFeed; isMobileVersion: boolean; + unreadStreamsCount?: number; + unreadMessages?: number; } export const ProjectFeedItem: FC = (props) => { - const { item, isMobileVersion } = props; + const { item, isMobileVersion, unreadStreamsCount, unreadMessages } = props; const history = useHistory(); const { getCommonPagePath } = useRoutesContext(); const { renderFeedItemBaseContent } = useFeedItemContext(); const { data: common, fetched: isCommonFetched, fetchCommon } = useCommon(); - const { unreadStreamsCount, unreadMessages } = useFeedItemCounters( - item.id, - common?.directParent?.commonId, - ); const commonId = item.data.id; const lastMessage = parseStringToTextEditorValue( Number.isInteger(unreadStreamsCount) diff --git a/src/pages/common/components/FeedItem/constants/feedItemMenuItem.ts b/src/pages/common/components/FeedItem/constants/feedItemMenuItem.ts index fd35388a18..2e6995333b 100644 --- a/src/pages/common/components/FeedItem/constants/feedItemMenuItem.ts +++ b/src/pages/common/components/FeedItem/constants/feedItemMenuItem.ts @@ -7,4 +7,6 @@ export enum FeedItemMenuItem { Remove = "remove", Follow = "follow", Unfollow = "unfollow", + MarkUnread = "markUnread", + MarkRead = "markRead", } diff --git a/src/pages/common/components/FeedItem/context.ts b/src/pages/common/components/FeedItem/context.ts index d4cae0d68d..67cf765193 100644 --- a/src/pages/common/components/FeedItem/context.ts +++ b/src/pages/common/components/FeedItem/context.ts @@ -26,6 +26,7 @@ export interface FeedItemBaseContentProps { menuItems?: ContextMenuItem[]; type?: CommonFeedType; seenOnce?: boolean; + seen?: boolean; ownerId?: string; commonName?: string; renderImage?: (className?: string) => ReactNode; diff --git a/src/pages/common/components/FeedItem/hooks/useFeedItemCounters.ts b/src/pages/common/components/FeedItem/hooks/useFeedItemCounters.ts index 3b362207db..0b1efacea5 100644 --- a/src/pages/common/components/FeedItem/hooks/useFeedItemCounters.ts +++ b/src/pages/common/components/FeedItem/hooks/useFeedItemCounters.ts @@ -3,8 +3,8 @@ import { useCommonMember } from "@/pages/OldCommon/hooks"; import { useGovernanceByCommonId } from "@/shared/hooks/useCases"; interface Return { - unreadStreamsCount?: number; - unreadMessages?: number; + projectUnreadStreamsCount?: number; + projectUnreadMessages?: number; } export const useFeedItemCounters = ( @@ -28,7 +28,7 @@ export const useFeedItemCounters = ( }, [fetchGovernance, commonId]); return { - unreadStreamsCount: streamsUnreadCountByProjectStream?.[feedItemId], - unreadMessages: unreadCountByProjectStream?.[feedItemId], + projectUnreadStreamsCount: streamsUnreadCountByProjectStream?.[feedItemId], + projectUnreadMessages: unreadCountByProjectStream?.[feedItemId], }; }; diff --git a/src/pages/common/components/FeedItem/types.ts b/src/pages/common/components/FeedItem/types.ts index dea8ade8a1..0745776e70 100644 --- a/src/pages/common/components/FeedItem/types.ts +++ b/src/pages/common/components/FeedItem/types.ts @@ -7,6 +7,7 @@ import { Discussion, Proposal, CommonFeedType, + CommonFeedObjectUserUnique, } from "@/shared/models"; import { FeedItemMenuItem } from "./constants"; @@ -25,6 +26,7 @@ export interface GetAllowedItemsOptions { commonMember?: CommonMember | null; feedItemFollow: FeedItemFollowState; getNonAllowedItems?: GetNonAllowedItemsOptions; + feedItemUserMetadata: CommonFeedObjectUserUnique | null; } export type MenuItemOptions = Omit; diff --git a/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx index 106f2bccc2..4284c62e37 100644 --- a/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx +++ b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx @@ -176,6 +176,7 @@ const ProposalFeedCard = forwardRef( commonMember, feedItemFollow, getNonAllowedItems, + feedItemUserMetadata, }, { report: () => {}, @@ -426,6 +427,7 @@ const ProposalFeedCard = forwardRef( isLoading={isLoading} type={item.data.type} seenOnce={feedItemUserMetadata?.seenOnce} + seen={feedItemUserMetadata?.seen} menuItems={menuItems} ownerId={item.userId} > diff --git a/src/pages/inbox/components/FeedItemBaseContent/FeedItemBaseContent.tsx b/src/pages/inbox/components/FeedItemBaseContent/FeedItemBaseContent.tsx index 67d2ffff44..04238c3472 100644 --- a/src/pages/inbox/components/FeedItemBaseContent/FeedItemBaseContent.tsx +++ b/src/pages/inbox/components/FeedItemBaseContent/FeedItemBaseContent.tsx @@ -27,6 +27,7 @@ export const FeedItemBaseContent: FC = (props) => { type, menuItems, seenOnce, + seen, ownerId, commonName, renderImage, @@ -155,6 +156,7 @@ export const FeedItemBaseContent: FC = (props) => { unreadMessages={unreadMessages} type={type} seenOnce={seenOnce} + seen={seen} ownerId={ownerId} isActive={isActive} isPinned={false} diff --git a/src/services/CommonFeed.ts b/src/services/CommonFeed.ts index c4269783b0..f9920217aa 100644 --- a/src/services/CommonFeed.ts +++ b/src/services/CommonFeed.ts @@ -220,6 +220,16 @@ class CommonFeedService { return convertObjectDatesToFirestoreTimestamps(data); }; + public markCommonFeedItemAsUnseen = ( + commonId: string, + feedObjectId: string, + ) => { + return Api.post(ApiEndpoint.MarkFeedObjectUnseenForUser, { + commonId, + feedObjectId, + }); + }; + public subscribeToCommonFeedItem = ( commonId: string, feedItemId: string, diff --git a/src/shared/constants/endpoint.ts b/src/shared/constants/endpoint.ts index e149c875b7..88af6d488c 100644 --- a/src/shared/constants/endpoint.ts +++ b/src/shared/constants/endpoint.ts @@ -6,6 +6,7 @@ export const ApiEndpoint = { UpdateCommon: "/commons/update", CreateSubCommon: "/commons/subcommon/create", MarkFeedObjectSeenForUser: "/commons/mark-feed-object-seen-for-user", + MarkFeedObjectUnseenForUser: "/commons/mark-feed-object-unseen-for-user", AcceptRules: "/commons/accept-rules", GetCommonFeedItems: "/commons/:commonId/feed-items", GetCommonPinnedFeedItems: "/commons/:commonId/pinned-feed-items", diff --git a/src/shared/icons/index.ts b/src/shared/icons/index.ts index 02183e0235..0af15350b7 100644 --- a/src/shared/icons/index.ts +++ b/src/shared/icons/index.ts @@ -44,6 +44,7 @@ export { default as UploadIcon } from "./upload.icon"; export { default as WalletIcon } from "./wallet.icon"; export { default as MessageIcon } from "./message.icon"; export { default as Message2Icon } from "./message2.icon"; +export { default as Message3Icon } from "./message3.icon"; export { default as Trash2Icon } from "./trash2.icon"; export { UnfollowIcon } from "./unfollow.icon"; export { UnpinIcon } from "./unpin.icon"; diff --git a/src/shared/icons/message3.icon.tsx b/src/shared/icons/message3.icon.tsx new file mode 100644 index 0000000000..8a48992260 --- /dev/null +++ b/src/shared/icons/message3.icon.tsx @@ -0,0 +1,37 @@ +import React, { FC } from "react"; + +interface Message3IconProps { + className?: string; +} + +const Message3Icon: FC = ({ className }) => { + const color = "currentColor"; + + return ( + + + + + ); +}; + +export default Message3Icon; diff --git a/src/shared/models/CommonFeedObjectUserUnique.ts b/src/shared/models/CommonFeedObjectUserUnique.ts index c37fcd5495..ba4ce206e7 100644 --- a/src/shared/models/CommonFeedObjectUserUnique.ts +++ b/src/shared/models/CommonFeedObjectUserUnique.ts @@ -4,7 +4,7 @@ import { Timestamp } from "./Timestamp"; export interface CommonFeedObjectUserUnique extends BaseEntity { userId: string; - lastSeen: { + lastSeen?: { type: LastSeenEntity; id: string; }; @@ -13,4 +13,5 @@ export interface CommonFeedObjectUserUnique extends BaseEntity { feedObjectId: string; commonId: string; seenOnce?: boolean; + seen?: boolean; }