diff --git a/src/pages/common/components/ChatComponent/context.ts b/src/pages/common/components/ChatComponent/context.ts index 2db6aeda1d..bf219498f4 100644 --- a/src/pages/common/components/ChatComponent/context.ts +++ b/src/pages/common/components/ChatComponent/context.ts @@ -15,6 +15,7 @@ export interface ChatItem { lastSeenItem?: CommonFeedObjectUserUnique["lastSeen"]; lastSeenAt?: CommonFeedObjectUserUnique["lastSeenAt"]; seenOnce?: boolean; + hasUnseenMention?: CommonFeedObjectUserUnique["hasUnseenMention"]; } export interface ChatContextValue { diff --git a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewStreamButton/NewStreamButton.tsx b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewStreamButton/NewStreamButton.tsx index ecd7a149a7..f39711d48c 100644 --- a/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewStreamButton/NewStreamButton.tsx +++ b/src/pages/common/components/CommonTabPanels/components/FeedTab/components/NewStreamButton/NewStreamButton.tsx @@ -1,9 +1,9 @@ import React, { FC } from "react"; -import { BoldPlusIcon } from "@/shared/icons"; +import { PlusButton } from "@/pages/inbox/components/HeaderContent/components"; +import { PlusIcon } from "@/shared/icons"; import { CirclesPermissions, CommonMember, Governance } from "@/shared/models"; import { Button, - ButtonIcon, ButtonSize, ButtonVariant, DesktopMenu, @@ -28,7 +28,7 @@ const NewStreamButton: FC = (props) => { } = props; const items = useMenuItems({ commonMember, governance }); const buttonVariant = ButtonVariant.OutlineDarkPink; - const iconEl = ; + const iconEl = ; if (items.length === 0) { return null; @@ -55,7 +55,7 @@ const NewStreamButton: FC = (props) => { return ( {iconEl}} + triggerEl={} items={items} /> ); diff --git a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx index 3ddf95d5c7..37ac83fd05 100644 --- a/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx +++ b/src/pages/common/components/DiscussionFeedCard/DiscussionFeedCard.tsx @@ -168,6 +168,7 @@ const DiscussionFeedCard = forwardRef( lastSeenItem: feedItemUserMetadata?.lastSeen, lastSeenAt: feedItemUserMetadata?.lastSeenAt, seenOnce: feedItemUserMetadata?.seenOnce, + hasUnseenMention: feedItemUserMetadata?.hasUnseenMention, }); } }, [ @@ -177,6 +178,7 @@ const DiscussionFeedCard = forwardRef( feedItemUserMetadata?.lastSeen, feedItemUserMetadata?.lastSeenAt, feedItemUserMetadata?.seenOnce, + feedItemUserMetadata?.hasUnseenMention, ]); const onDiscussionDelete = useCallback(async () => { @@ -333,6 +335,10 @@ const DiscussionFeedCard = forwardRef( seen={feedItemUserMetadata?.seen ?? !isFeedItemUserMetadataFetched} ownerId={item.userId} discussionPredefinedType={discussion?.predefinedType} + hasUnseenMention={ + feedItemUserMetadata?.hasUnseenMention ?? + !isFeedItemUserMetadataFetched + } > {renderContent()} diff --git a/src/pages/common/components/FeedCard/FeedCard.tsx b/src/pages/common/components/FeedCard/FeedCard.tsx index 84011cb894..8e593dc061 100644 --- a/src/pages/common/components/FeedCard/FeedCard.tsx +++ b/src/pages/common/components/FeedCard/FeedCard.tsx @@ -45,6 +45,7 @@ type FeedCardProps = PropsWithChildren<{ discussionPredefinedType?: PredefinedTypes; hasFiles?: boolean; hasImages?: boolean; + hasUnseenMention?: boolean; }>; const MOBILE_HEADER_HEIGHT = 52; @@ -79,6 +80,7 @@ export const FeedCard = forwardRef((props, ref) => { menuItems, seenOnce, seen, + hasUnseenMention, ownerId, discussionPredefinedType, hasImages, @@ -204,6 +206,7 @@ export const FeedCard = forwardRef((props, ref) => { discussionPredefinedType, hasFiles, hasImages, + hasUnseenMention, })} )} 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 0f0985d0b5..92d2c46860 100644 --- a/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.module.scss +++ b/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.module.scss @@ -56,3 +56,7 @@ border-radius: 50%; background-color: $c-pink-primary; } + +.hasUnseenMention { + 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 eb69e59e8e..168f1182d8 100644 --- a/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx +++ b/src/pages/common/components/FeedCard/components/FeedCardTags/FeedCardTags.tsx @@ -16,6 +16,7 @@ interface FeedCardTagsProps { isActive: boolean; isPinned?: boolean; isFollowing?: boolean; + hasUnseenMention?: boolean; } export const FeedCardTags: FC = (props) => { @@ -28,6 +29,7 @@ export const FeedCardTags: FC = (props) => { isActive, isPinned, isFollowing, + hasUnseenMention, } = props; const user = useSelector(selectUser()); const isOwner = ownerId === user?.uid; @@ -54,6 +56,7 @@ export const FeedCardTags: FC = (props) => { })} /> )} + {hasUnseenMention &&
@
} {isFollowing && ( )} diff --git a/src/pages/common/components/FeedCard/components/FeedItemBaseContent/FeedItemBaseContent.tsx b/src/pages/common/components/FeedCard/components/FeedItemBaseContent/FeedItemBaseContent.tsx index f51be70090..2324322854 100644 --- a/src/pages/common/components/FeedCard/components/FeedItemBaseContent/FeedItemBaseContent.tsx +++ b/src/pages/common/components/FeedCard/components/FeedItemBaseContent/FeedItemBaseContent.tsx @@ -35,6 +35,7 @@ export const FeedItemBaseContent: FC = (props) => { isFollowing, isLoading = false, shouldHideBottomContent = false, + hasUnseenMention, } = props; const contextMenuRef = useRef(null); const [isLongPressing, setIsLongPressing] = useState(false); @@ -154,6 +155,7 @@ export const FeedItemBaseContent: FC = (props) => { isActive={isActive} isPinned={isPinned} isFollowing={isFollowing} + hasUnseenMention={hasUnseenMention} /> diff --git a/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx b/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx index ae282dc417..3b4624f969 100644 --- a/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx +++ b/src/pages/common/components/FeedItem/components/ProjectFeedItem/ProjectFeedItem.tsx @@ -3,7 +3,7 @@ import { useHistory } from "react-router-dom"; import classNames from "classnames"; import { useFeedItemContext } from "@/pages/common"; import { useRoutesContext } from "@/shared/contexts"; -import { useCommon } from "@/shared/hooks/useCases"; +import { useCommon, useFeedItemFollow } from "@/shared/hooks/useCases"; import { OpenIcon } from "@/shared/icons"; import { CommonFeed } from "@/shared/models"; import { CommonAvatar, parseStringToTextEditorValue } from "@/shared/ui-kit"; @@ -22,6 +22,10 @@ export const ProjectFeedItem: FC = (props) => { const { getCommonPagePath } = useRoutesContext(); const { renderFeedItemBaseContent } = useFeedItemContext(); const { data: common, fetched: isCommonFetched, fetchCommon } = useCommon(); + const feedItemFollow = useFeedItemFollow( + { feedItemId: item.id, commonId: item.data.id }, + { withSubscription: true }, + ); const { projectUnreadStreamsCount: unreadStreamsCount, projectUnreadMessages: unreadMessages, @@ -76,6 +80,7 @@ export const ProjectFeedItem: FC = (props) => { lastMessage, renderLeftContent, shouldHideBottomContent: !lastMessage, + isFollowing: feedItemFollow.isFollowing, })} ) || null diff --git a/src/pages/common/components/FeedItem/context.ts b/src/pages/common/components/FeedItem/context.ts index 67cf765193..38f3f3c557 100644 --- a/src/pages/common/components/FeedItem/context.ts +++ b/src/pages/common/components/FeedItem/context.ts @@ -42,6 +42,7 @@ export interface FeedItemBaseContentProps { hasImages?: boolean; isLoading?: boolean; shouldHideBottomContent?: boolean; + hasUnseenMention?: boolean; } export interface GetLastMessageOptions { diff --git a/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx index 934cd0720d..ddbc1c2414 100644 --- a/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx +++ b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx @@ -270,6 +270,7 @@ const ProposalFeedCard = forwardRef( lastSeenItem: feedItemUserMetadata?.lastSeen, lastSeenAt: feedItemUserMetadata?.lastSeenAt, seenOnce: feedItemUserMetadata?.seenOnce, + hasUnseenMention: feedItemUserMetadata?.hasUnseenMention, }); } }, [ @@ -281,6 +282,7 @@ const ProposalFeedCard = forwardRef( feedItemUserMetadata?.lastSeen, feedItemUserMetadata?.lastSeenAt, feedItemUserMetadata?.seenOnce, + feedItemUserMetadata?.hasUnseenMention, ]); useEffect(() => { @@ -458,6 +460,10 @@ const ProposalFeedCard = forwardRef( seen={feedItemUserMetadata?.seen ?? !isFeedItemUserMetadataFetched} menuItems={menuItems} ownerId={item.userId} + hasUnseenMention={ + feedItemUserMetadata?.hasUnseenMention ?? + !isFeedItemUserMetadataFetched + } > {renderContent()} diff --git a/src/pages/inbox/BaseInbox.tsx b/src/pages/inbox/BaseInbox.tsx index 95f1de212e..8be5cfa57e 100644 --- a/src/pages/inbox/BaseInbox.tsx +++ b/src/pages/inbox/BaseInbox.tsx @@ -9,6 +9,7 @@ import React, { } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; +import { useUpdateEffect } from "react-use"; import { selectUser } from "@/pages/Auth/store/selectors"; import { FeedItemBaseContentProps } from "@/pages/common"; import { @@ -67,6 +68,8 @@ const InboxPage: FC = (props) => { const [feedLayoutRef, setFeedLayoutRef] = useState( null, ); + const isActiveUnreadInboxItemsQueryParam = + queryParams[QueryParamKey.Unread] === "true"; const sharedFeedItemIdQueryParam = queryParams[QueryParamKey.Item]; const sharedFeedItemId = (typeof sharedFeedItemIdQueryParam === "string" && @@ -88,9 +91,10 @@ const InboxPage: FC = (props) => { loading: areInboxItemsLoading, hasMore: hasMoreInboxItems, fetch: fetchInboxItems, + refetch: refetchInboxItems, batchNumber, } = useInboxItems(feedItemIdsForNotListening, { - unread: queryParams.unread === "true", + unread: isActiveUnreadInboxItemsQueryParam, }); const sharedInboxItem = useSelector(selectSharedInboxItem); const chatChannelItems = useSelector(selectChatChannelItems); @@ -108,6 +112,10 @@ const InboxPage: FC = (props) => { return items; }, [chatChannelItems, sharedInboxItem]); + useUpdateEffect(() => { + refetchInboxItems(); + }, [isActiveUnreadInboxItemsQueryParam]); + const fetchData = () => { fetchInboxData({ sharedFeedItemId, @@ -264,7 +272,11 @@ const InboxPage: FC = (props) => { renderChatChannelItem={renderChatChannelItem} onFeedItemUpdate={handleFeedItemUpdate} getLastMessage={getLastMessage} - emptyText="Your inbox is empty" + emptyText={ + isActiveUnreadInboxItemsQueryParam + ? "Hurry! No unread items in your inbox :-)" + : "Your inbox is empty" + } getNonAllowedItems={getNonAllowedItems} onActiveItemChange={handleActiveItemChange} onActiveItemDataChange={onActiveItemDataChange} diff --git a/src/pages/inbox/components/FeedItemBaseContent/FeedItemBaseContent.tsx b/src/pages/inbox/components/FeedItemBaseContent/FeedItemBaseContent.tsx index 04238c3472..ef5da01dba 100644 --- a/src/pages/inbox/components/FeedItemBaseContent/FeedItemBaseContent.tsx +++ b/src/pages/inbox/components/FeedItemBaseContent/FeedItemBaseContent.tsx @@ -36,6 +36,7 @@ export const FeedItemBaseContent: FC = (props) => { isImageRounded, isProject, discussionPredefinedType, + hasUnseenMention, } = props; const contextMenuRef = useRef(null); const [isLongPressing, setIsLongPressing] = useState(false); @@ -160,6 +161,7 @@ export const FeedItemBaseContent: FC = (props) => { ownerId={ownerId} isActive={isActive} isPinned={false} + hasUnseenMention={hasUnseenMention} /> diff --git a/src/pages/inbox/components/HeaderContent/HeaderContent.tsx b/src/pages/inbox/components/HeaderContent/HeaderContent.tsx index be50571f40..1627e0fcbd 100644 --- a/src/pages/inbox/components/HeaderContent/HeaderContent.tsx +++ b/src/pages/inbox/components/HeaderContent/HeaderContent.tsx @@ -4,6 +4,7 @@ import { useIsTabletView } from "@/shared/hooks/viewport"; import { InboxIcon } from "@/shared/icons"; import { DirectMessageButton } from "../DirectMessageButton"; import { HeaderContent_v04 } from "../HeaderContent_v04"; +import { InboxFilterButton } from "../InboxFilterButton"; import { PlusButton } from "./components"; import styles from "./HeaderContent.module.scss"; @@ -32,6 +33,7 @@ const HeaderContent: FC = (props) => {

Inbox

+ * { + margin-right: 1rem; + + &:last-child { + margin-right: 0; + } + } +} diff --git a/src/pages/inbox/components/HeaderContent_v04/HeaderContent_v04.tsx b/src/pages/inbox/components/HeaderContent_v04/HeaderContent_v04.tsx index 911d2f13b0..1697379141 100644 --- a/src/pages/inbox/components/HeaderContent_v04/HeaderContent_v04.tsx +++ b/src/pages/inbox/components/HeaderContent_v04/HeaderContent_v04.tsx @@ -4,6 +4,8 @@ import { useIsTabletView } from "@/shared/hooks/viewport"; import { InboxIcon } from "@/shared/icons"; import { getPluralEnding } from "@/shared/utils"; import { DirectMessageButton } from "../DirectMessageButton"; +import { PlusButton } from "../HeaderContent/components"; +import { InboxFilterButton } from "../InboxFilterButton"; import styles from "./HeaderContent_v04.module.scss"; interface HeaderContentProps { @@ -29,10 +31,18 @@ const HeaderContent_v04: FC = (props) => {
- +
+ + + +
); }; diff --git a/src/pages/inbox/components/InboxFilterButton/InboxFilterButton.module.scss b/src/pages/inbox/components/InboxFilterButton/InboxFilterButton.module.scss new file mode 100644 index 0000000000..a4e15ac9d5 --- /dev/null +++ b/src/pages/inbox/components/InboxFilterButton/InboxFilterButton.module.scss @@ -0,0 +1,29 @@ +@import "../../../../constants"; +@import "../../../../styles/sizes"; + +.buttonIcon { + background-color: transparent; + + @media (hover: hover) and (pointer: fine) { + &:hover { + background-color: $light-gray-1; + } + } +} + +.unreadFilterActive { + background-color: $c-pink-next-dark; + color: $white; + + @media (hover: hover) and (pointer: fine) { + &:hover { + background-color: $c-pink-mention-2; + } + } +} + +.icon { + width: 1.5rem; + height: 1.5rem; + color: inherit; +} \ No newline at end of file diff --git a/src/pages/inbox/components/InboxFilterButton/InboxFilterButton.tsx b/src/pages/inbox/components/InboxFilterButton/InboxFilterButton.tsx new file mode 100644 index 0000000000..26a84f6833 --- /dev/null +++ b/src/pages/inbox/components/InboxFilterButton/InboxFilterButton.tsx @@ -0,0 +1,47 @@ +import React, { FC } from "react"; +import { useHistory, useLocation } from "react-router"; +import classnames from "classnames"; +import { QueryParamKey } from "@/shared/constants"; +import { useQueryParams, useRemoveQueryParams } from "@/shared/hooks"; +import { InboxFilterIcon } from "@/shared/icons"; +import { ButtonIcon } from "@/shared/ui-kit"; +import styles from "./InboxFilterButton.module.scss"; + +interface InboxFilterButtonProps { + className?: string; +} + +const InboxFilterButton: FC = (props) => { + const { className } = props; + const history = useHistory(); + const location = useLocation(); + const queryParams = useQueryParams(); + const { removeQueryParams } = useRemoveQueryParams(); + const isActiveUnreadInboxItemsQueryParam = + queryParams[QueryParamKey.Unread] === "true"; + + const handleFilterIconClick = (): void => { + if (isActiveUnreadInboxItemsQueryParam) { + removeQueryParams(QueryParamKey.Unread); + } else { + history.push(`${location.pathname}?${QueryParamKey.Unread}=true`); + } + }; + + return ( + + + + ); +}; + +export default InboxFilterButton; diff --git a/src/pages/inbox/components/InboxFilterButton/index.ts b/src/pages/inbox/components/InboxFilterButton/index.ts new file mode 100644 index 0000000000..60fdf9c83e --- /dev/null +++ b/src/pages/inbox/components/InboxFilterButton/index.ts @@ -0,0 +1 @@ +export { default as InboxFilterButton } from "./InboxFilterButton"; \ No newline at end of file diff --git a/src/shared/hooks/useCases/useInboxItems.ts b/src/shared/hooks/useCases/useInboxItems.ts index 98c2426492..f91ecd7967 100644 --- a/src/shared/hooks/useCases/useInboxItems.ts +++ b/src/shared/hooks/useCases/useInboxItems.ts @@ -16,6 +16,7 @@ import { inboxActions, InboxItems, selectInboxItems } from "@/store/states"; interface Return extends Pick { fetch: () => void; + refetch: () => void; } interface ItemBatch { @@ -129,6 +130,11 @@ export const useInboxItems = ( ); }; + const refetch = () => { + dispatch(inboxActions.resetInboxItems()); + fetch(); + } + useEffect(() => { if (!inboxItems.firstDocTimestamp || !userId) { return; @@ -220,5 +226,6 @@ export const useInboxItems = ( return { ...inboxItems, fetch, + refetch, }; }; diff --git a/src/shared/icons/inboxFilter.icon.tsx b/src/shared/icons/inboxFilter.icon.tsx new file mode 100644 index 0000000000..52ac490c20 --- /dev/null +++ b/src/shared/icons/inboxFilter.icon.tsx @@ -0,0 +1,51 @@ +import React, { FC } from "react"; + +interface InboxFilterIconProps { + className?: string; +} + +const InboxFilterIcon: FC = ({ className }) => { + return ( + + + + + + + ); +}; + +export default InboxFilterIcon; diff --git a/src/shared/icons/index.ts b/src/shared/icons/index.ts index 9f9a8fb7b3..47872eacb9 100644 --- a/src/shared/icons/index.ts +++ b/src/shared/icons/index.ts @@ -67,3 +67,4 @@ export { default as EmojiIcon } from "./emoji.icon"; export { default as ReplyIcon } from "./reply.icon"; export { default as CopyIcon } from "./copy.icon"; export { default as HideIcon } from "./hide.icon"; +export { default as InboxFilterIcon } from "./inboxFilter.icon"; diff --git a/src/shared/models/CommonFeedObjectUserUnique.ts b/src/shared/models/CommonFeedObjectUserUnique.ts index ba4ce206e7..97fd50cbb5 100644 --- a/src/shared/models/CommonFeedObjectUserUnique.ts +++ b/src/shared/models/CommonFeedObjectUserUnique.ts @@ -14,4 +14,5 @@ export interface CommonFeedObjectUserUnique extends BaseEntity { commonId: string; seenOnce?: boolean; seen?: boolean; + hasUnseenMention?: boolean; }