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 e7a40b7f59..acdc57a258 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 () => { @@ -334,6 +336,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 b2d428e9a0..f351559388 100644 --- a/src/pages/common/components/FeedCard/FeedCard.tsx +++ b/src/pages/common/components/FeedCard/FeedCard.tsx @@ -46,6 +46,7 @@ type FeedCardProps = PropsWithChildren<{ discussionPredefinedType?: PredefinedTypes; hasFiles?: boolean; hasImages?: boolean; + hasUnseenMention?: boolean; }>; const MOBILE_HEADER_HEIGHT = 52; @@ -81,6 +82,7 @@ export const FeedCard = forwardRef((props, ref) => { menuItems, seenOnce, seen, + hasUnseenMention, ownerId, discussionPredefinedType, hasImages, @@ -207,6 +209,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 db388eb6fb..3f408ee137 100644 --- a/src/pages/common/components/FeedItem/context.ts +++ b/src/pages/common/components/FeedItem/context.ts @@ -44,6 +44,7 @@ export interface FeedItemBaseContentProps { isLoading?: boolean; shouldHideBottomContent?: boolean; dmUserId?: string; + hasUnseenMention?: boolean; } export interface GetLastMessageOptions { diff --git a/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx b/src/pages/common/components/ProposalFeedCard/ProposalFeedCard.tsx index 6259c659ef..e585b92953 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(() => { @@ -459,6 +461,10 @@ const ProposalFeedCard = forwardRef( menuItems={menuItems} ownerId={item.userId} commonId={commonId} + 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 56055f4658..40ca9f1701 100644 --- a/src/pages/inbox/components/FeedItemBaseContent/FeedItemBaseContent.tsx +++ b/src/pages/inbox/components/FeedItemBaseContent/FeedItemBaseContent.tsx @@ -44,6 +44,7 @@ export const FeedItemBaseContent: FC = (props) => { discussionPredefinedType, dmUserId, commonId, + hasUnseenMention, } = props; const history = useHistory(); const { getCommonPagePath } = useRoutesContext(); @@ -181,6 +182,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..5f1116d826 --- /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; +} 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..7954a1b731 --- /dev/null +++ b/src/pages/inbox/components/InboxFilterButton/index.ts @@ -0,0 +1 @@ +export { default as InboxFilterButton } from "./InboxFilterButton"; diff --git a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx index e49ce29b03..3d4a50379f 100644 --- a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx +++ b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx @@ -7,6 +7,7 @@ import React, { } from "react"; import classNames from "classnames"; import { useLongPress } from "use-long-press"; +import { Logger } from "@/services"; import { ElementDropdown, UserAvatar, @@ -110,6 +111,7 @@ export default function ChatMessage({ !isUserDiscussionMessage || userId !== discussionMessageUserId; const [messageText, setMessageText] = useState<(string | JSX.Element)[]>([]); + const [isMessageDataFetching, setIsMessageDataFetching] = useState(false); const [replyMessageText, setReplyMessageText] = useState< (string | JSX.Element)[] @@ -144,26 +146,35 @@ export default function ChatMessage({ const emojiCount = countTextEditorEmojiElements( parseStringToTextEditorValue(discussionMessage.text), ); - const parsedText = await getTextFromTextEditorString({ - textEditorString: discussionMessage.text, - users, - mentionTextClassName: !isNotCurrentUserMessage - ? styles.mentionTextCurrentUser - : "", - emojiTextClassName: classNames({ - [styles.singleEmojiText]: emojiCount.isSingleEmoji, - [styles.multipleEmojiText]: emojiCount.isMultipleEmoji, - }), - commonId: discussionMessage.commonId, - systemMessage: isSystemMessage ? discussionMessage : undefined, - getCommonPagePath, - getCommonPageAboutTabPath, - directParent, - onUserClick, - onFeedItemClick, - }); - setMessageText(parsedText); + setIsMessageDataFetching(true); + + try { + const parsedText = await getTextFromTextEditorString({ + textEditorString: discussionMessage.text, + users, + mentionTextClassName: !isNotCurrentUserMessage + ? styles.mentionTextCurrentUser + : "", + emojiTextClassName: classNames({ + [styles.singleEmojiText]: emojiCount.isSingleEmoji, + [styles.multipleEmojiText]: emojiCount.isMultipleEmoji, + }), + commonId: discussionMessage.commonId, + systemMessage: isSystemMessage ? discussionMessage : undefined, + getCommonPagePath, + getCommonPageAboutTabPath, + directParent, + onUserClick, + onFeedItemClick, + }); + + setMessageText(parsedText); + } catch (error) { + Logger.error(error); + } finally { + setIsMessageDataFetching(false); + } })(); }, [ users, @@ -401,7 +412,7 @@ export default function ChatMessage({ - {!messageText.length ? ( + {!messageText.length && isMessageDataFetching ? ( Loading... ) : ( messageText.map((text) => text) diff --git a/src/shared/hooks/useCases/useInboxItems.ts b/src/shared/hooks/useCases/useInboxItems.ts index 98c2426492..e9329172d4 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; }