From 56b9da48401218a0ab5a8d5fbd7c9933be3c7651 Mon Sep 17 00:00:00 2001 From: Pavel Meyer Date: Sat, 2 Nov 2024 01:52:15 +0300 Subject: [PATCH] CW-mention-streams Added mentions streams Added Stream mention component --- package.json | 1 + src/pages/App/App.tsx | 31 ++++++++----- .../ChatComponent/ChatComponent.tsx | 9 ++++ .../components/ChatContent/ChatContent.tsx | 4 ++ .../components/ChatInput/ChatInput.tsx | 5 +- .../hooks/useDiscussionChatAdapter.ts | 3 ++ .../common/components/FeedItem/FeedItem.tsx | 1 + src/pages/commonFeed/CommonFeed.tsx | 2 +- .../components/DesktopChat/DesktopChat.tsx | 1 + .../components/MobileChat/MobileChat.tsx | 1 + src/services/CommonFeed.ts | 15 ++++++ src/services/Discussion.ts | 11 +++++ .../Chat/ChatMessage/ChatMessage.tsx | 5 ++ .../Chat/ChatMessage/DMChatMessage.tsx | 6 +++ .../StreamMention/StreamMention.tsx | 46 +++++++++++++++++++ .../components/StreamMention/index.ts | 1 + .../Chat/ChatMessage/components/index.ts | 1 + .../components/Chat/ChatMessage/types.ts | 1 + .../utils/getTextFromTextEditorString.tsx | 20 +++++++- src/shared/hooks/index.tsx | 1 + .../useCases/useDiscussionMessagesById.ts | 7 +++ .../usePreloadDiscussionMessagesById.ts | 3 ++ .../hooks/useFetchDiscussionsByCommonId.tsx | 13 ++++++ .../ui-kit/TextEditor/BaseTextEditor.tsx | 18 +++++++- .../TextEditor/components/Element/Element.tsx | 22 +++++++++ .../MentionDropdown/MentionDropdown.tsx | 40 +++++++++++++--- .../TextEditor/constants/elementType.ts | 2 + .../ui-kit/TextEditor/hofs/withMentions.ts | 4 +- src/shared/ui-kit/TextEditor/types.ts | 8 ++++ .../utils/checkIsTextEditorValueEmpty.ts | 2 +- src/shared/ui-kit/TextEditor/utils/index.ts | 1 + .../TextEditor/utils/insertStreamMention.ts | 18 ++++++++ .../removeTextEditorEmptyEndLinesValues.ts | 1 + yarn.lock | 18 ++++++++ 34 files changed, 297 insertions(+), 25 deletions(-) create mode 100644 src/shared/components/Chat/ChatMessage/components/StreamMention/StreamMention.tsx create mode 100644 src/shared/components/Chat/ChatMessage/components/StreamMention/index.ts create mode 100644 src/shared/hooks/useFetchDiscussionsByCommonId.tsx create mode 100644 src/shared/ui-kit/TextEditor/utils/insertStreamMention.ts diff --git a/package.json b/package.json index 1553bfedca..b662b1e6db 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@floating-ui/react-dom-interactions": "^0.13.1", "@headlessui/react": "^1.7.4", "@storybook/addon-viewport": "^6.5.13", + "@tanstack/react-query": "4.5.0", "@tanstack/react-table": "^8.7.9", "@types/react-pdf": "^5.7.2", "axios": "^0.21.0", diff --git a/src/pages/App/App.tsx b/src/pages/App/App.tsx index 5e0c242064..7e0f058f61 100644 --- a/src/pages/App/App.tsx +++ b/src/pages/App/App.tsx @@ -16,6 +16,13 @@ import { NotificationsHandler, } from "./handlers"; import { Router } from "./router"; +import { + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query' + +// Create a client +const queryClient = new QueryClient() const App = () => { const dispatch = useDispatch(); @@ -28,17 +35,19 @@ const App = () => { }, [dispatch, isDesktop]); return ( - - - - - - - - - - - + + + + + + + + + + + + + ); }; diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx index 5bd1d392da..bed47c5079 100644 --- a/src/pages/common/components/ChatComponent/ChatComponent.tsx +++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx @@ -30,6 +30,7 @@ import { useZoomDisabling, useImageSizeCheck, useQueryParams, + useFetchDiscussionsByCommonId, } from "@/shared/hooks"; import { ArrowInCircleIcon } from "@/shared/icons"; import { LinkPreviewData } from "@/shared/interfaces"; @@ -108,6 +109,7 @@ interface ChatComponentInterface { directParent?: DirectParent | null; renderChatInput?: () => ReactNode; onUserClick?: (userId: string) => void; + onStreamMentionClick?: (feedItemId: string) => void; onFeedItemClick?: (feedItemId: string) => void; onInternalLinkClick?: (data: InternalLinkData) => void; } @@ -156,6 +158,7 @@ export default function ChatComponent({ directParent, renderChatInput: renderChatInputOuter, onUserClick, + onStreamMentionClick, onFeedItemClick, onInternalLinkClick, }: ChatComponentInterface) { @@ -202,6 +205,7 @@ export default function ChatComponent({ }, onFeedItemClick, onUserClick, + onStreamMentionClick, commonId, onInternalLinkClick, }); @@ -215,6 +219,9 @@ export default function ChatComponent({ chatChannelId: chatChannel?.id || "", participants: chatChannel?.participants, }); + + const {data: discussionsData} = useFetchDiscussionsByCommonId(commonId); + const users = useMemo( () => (chatChannel ? chatUsers : discussionUsers), [chatUsers, discussionUsers, chatChannel], @@ -827,6 +834,7 @@ export default function ChatComponent({ onMessageDelete={handleMessageDelete} directParent={directParent} onUserClick={onUserClick} + onStreamMentionClick={onStreamMentionClick} onFeedItemClick={onFeedItemClick} onInternalLinkClick={onInternalLinkClick} isEmpty={ @@ -864,6 +872,7 @@ export default function ChatComponent({ onClearFinished={onClearFinished} shouldReinitializeEditor={shouldReinitializeEditor} users={users} + discussions={discussionsData ?? []} onEnterKeyDown={onEnterKeyDown} emojiCount={emojiCount} setMessage={setMessage} diff --git a/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx b/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx index 41c6772672..819964cb81 100644 --- a/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx +++ b/src/pages/common/components/ChatComponent/components/ChatContent/ChatContent.tsx @@ -68,6 +68,7 @@ interface ChatContentInterface { onMessageDelete?: (messageId: string) => void; directParent?: DirectParent | null; onUserClick?: (userId: string) => void; + onStreamMentionClick?: (link: string) => void; onFeedItemClick?: (feedItemId: string) => void; onInternalLinkClick?: (data: InternalLinkData) => void; isEmpty?: boolean; @@ -106,6 +107,7 @@ const ChatContent: ForwardRefRenderFunction< onMessageDelete, directParent, onUserClick, + onStreamMentionClick, onFeedItemClick, onInternalLinkClick, isEmpty, @@ -292,6 +294,7 @@ const ChatContent: ForwardRefRenderFunction< onMessageDelete={onMessageDelete} directParent={directParent} onUserClick={onUserClick} + onStreamMentionClick={onStreamMentionClick} onFeedItemClick={onFeedItemClick} onInternalLinkClick={onInternalLinkClick} chatChannelId={chatChannelId} @@ -312,6 +315,7 @@ const ChatContent: ForwardRefRenderFunction< onMessageDelete={onMessageDelete} directParent={directParent} onUserClick={onUserClick} + onStreamMentionClick={onStreamMentionClick} onFeedItemClick={onFeedItemClick} onInternalLinkClick={onInternalLinkClick} isMessageEditAllowed={isMessageEditAllowed} diff --git a/src/pages/common/components/ChatComponent/components/ChatInput/ChatInput.tsx b/src/pages/common/components/ChatComponent/components/ChatInput/ChatInput.tsx index d51f9098dd..f80f128a58 100644 --- a/src/pages/common/components/ChatComponent/components/ChatInput/ChatInput.tsx +++ b/src/pages/common/components/ChatComponent/components/ChatInput/ChatInput.tsx @@ -8,7 +8,7 @@ import React, { import classNames from "classnames"; import { FILES_ACCEPTED_EXTENSIONS } from "@/shared/constants"; import { PlusIcon, SendIcon } from "@/shared/icons"; -import { User } from "@/shared/models"; +import { Discussion, User } from "@/shared/models"; import { BaseTextEditor, ButtonIcon, @@ -30,6 +30,7 @@ interface ChatInputProps { emojiCount: EmojiCount; onEnterKeyDown: (event: React.KeyboardEvent) => void; users: User[]; + discussions: Discussion[]; shouldReinitializeEditor: boolean; onClearFinished: () => void; canSendMessage?: boolean; @@ -58,6 +59,7 @@ export const ChatInput = React.memo(forwardRef void; + onStreamMentionClick?: (feedItemId: string) => void; onFeedItemClick?: (feedItemId: string) => void; onInternalLinkClick?: (data: InternalLinkData) => void; directParent?: DirectParent | null; @@ -37,6 +38,7 @@ export const useDiscussionChatAdapter = (options: Options): Return => { discussionId, onFeedItemClick, onUserClick, + onStreamMentionClick, commonId, onInternalLinkClick, } = options; @@ -63,6 +65,7 @@ export const useDiscussionChatAdapter = (options: Options): Return => { textStyles, onFeedItemClick, onUserClick, + onStreamMentionClick, onInternalLinkClick, }); const { markFeedItemAsSeen } = useUpdateFeedItemSeenState(); diff --git a/src/pages/common/components/FeedItem/FeedItem.tsx b/src/pages/common/components/FeedItem/FeedItem.tsx index 15a8d10276..4ee33282d1 100644 --- a/src/pages/common/components/FeedItem/FeedItem.tsx +++ b/src/pages/common/components/FeedItem/FeedItem.tsx @@ -174,6 +174,7 @@ const FeedItem = forwardRef((props, ref) => { shouldPreLoadMessages, withoutMenu, onUserClick: handleUserClick, + onStreamMentionClick: onFeedItemClick, onFeedItemClick, onInternalLinkClick, }), diff --git a/src/pages/commonFeed/CommonFeed.tsx b/src/pages/commonFeed/CommonFeed.tsx index de5643757b..2d87529367 100644 --- a/src/pages/commonFeed/CommonFeed.tsx +++ b/src/pages/commonFeed/CommonFeed.tsx @@ -129,7 +129,7 @@ const CommonFeedComponent: FC = (props) => { const anotherCommonId = userCommonIds[0] === commonId ? userCommonIds[1] : userCommonIds[0]; const pinnedItemIds = useMemo( - () => commonData?.common.pinnedFeedItems.map((item) => item.feedObjectId), + () => (commonData?.common.pinnedFeedItems ?? []).map((item) => item.feedObjectId), [commonData?.common.pinnedFeedItems], ); diff --git a/src/pages/commonFeed/components/FeedLayout/components/DesktopChat/DesktopChat.tsx b/src/pages/commonFeed/components/FeedLayout/components/DesktopChat/DesktopChat.tsx index 99bffc75d0..73a15ed04a 100644 --- a/src/pages/commonFeed/components/FeedLayout/components/DesktopChat/DesktopChat.tsx +++ b/src/pages/commonFeed/components/FeedLayout/components/DesktopChat/DesktopChat.tsx @@ -131,6 +131,7 @@ const DesktopChat: FC = (props) => { directParent={directParent} renderChatInput={renderChatInput} onUserClick={onUserClick} + onStreamMentionClick={onFeedItemClick} onFeedItemClick={onFeedItemClick} onInternalLinkClick={onInternalLinkClick} /> diff --git a/src/pages/commonFeed/components/FeedLayout/components/MobileChat/MobileChat.tsx b/src/pages/commonFeed/components/FeedLayout/components/MobileChat/MobileChat.tsx index aa6cbcaebd..62a1f3244b 100644 --- a/src/pages/commonFeed/components/FeedLayout/components/MobileChat/MobileChat.tsx +++ b/src/pages/commonFeed/components/FeedLayout/components/MobileChat/MobileChat.tsx @@ -167,6 +167,7 @@ const MobileChat: FC = (props) => { directParent={directParent} renderChatInput={renderChatInput} onUserClick={onUserClick} + onStreamMentionClick={onFeedItemClick} onFeedItemClick={onFeedItemClick} onInternalLinkClick={onInternalLinkClick} /> diff --git a/src/services/CommonFeed.ts b/src/services/CommonFeed.ts index 2e2f4d31d8..32e30c024c 100644 --- a/src/services/CommonFeed.ts +++ b/src/services/CommonFeed.ts @@ -19,6 +19,7 @@ import { CommonFeedObjectUserUnique, CommonFeedType, CommonMember, + FeedItemFollow, SubCollections, Timestamp, } from "@/shared/models"; @@ -318,6 +319,20 @@ class CommonFeedService { } }); }; + + + public getFeedItemByCommonAndDiscussionId = async ({commonId, discussionId}: {commonId: string; discussionId: string}): Promise => { + try { + const feedItems = await this.getCommonFeedSubCollection(commonId) + .where("data.id", "==", discussionId) + .get(); + + const data = feedItems.docs.map(doc => doc.data()); + return data?.[0]; + } catch (error) { + return null; + } + }; } export default new CommonFeedService(); diff --git a/src/services/Discussion.ts b/src/services/Discussion.ts index 778f6b1aec..7f4a8520c4 100644 --- a/src/services/Discussion.ts +++ b/src/services/Discussion.ts @@ -112,6 +112,17 @@ class DiscussionService { public deleteDiscussion = async (discussionId: string): Promise => { await Api.delete(ApiEndpoint.DeleteDiscussion(discussionId)); }; + + public getDiscussionsByCommonId = async (commonId: string) => { + const discussionCollection = await this.getDiscussionCollection() + .where("commonId", "==", commonId) // Query for documents where commonId matches + .get(); + + // Map the Firestore document data + const data = discussionCollection.docs.map(doc => doc.data()); + return data; + }; + } export default new DiscussionService(); diff --git a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx index 204f5a5964..9d1438d928 100644 --- a/src/shared/components/Chat/ChatMessage/ChatMessage.tsx +++ b/src/shared/components/Chat/ChatMessage/ChatMessage.tsx @@ -78,6 +78,7 @@ interface ChatMessageProps { onMessageDelete?: (messageId: string) => void; directParent?: DirectParent | null; onUserClick?: (userId: string) => void; + onStreamMentionClick?: (feedItemID: string) => void; onFeedItemClick?: (feedItemId: string) => void; onInternalLinkClick?: (data: InternalLinkData) => void; isMessageEditAllowed: boolean; @@ -109,6 +110,7 @@ const ChatMessage = ({ onMessageDelete, directParent, onUserClick, + onStreamMentionClick, onFeedItemClick, onInternalLinkClick, isMessageEditAllowed, @@ -165,6 +167,7 @@ const ChatMessage = ({ directParent, onUserClick, onFeedItemClick, + onStreamMentionClick, onInternalLinkClick, }); @@ -177,6 +180,7 @@ const ChatMessage = ({ isNotCurrentUserMessage, discussionMessage.commonId, onUserClick, + onStreamMentionClick, onInternalLinkClick, ]); @@ -302,6 +306,7 @@ const ChatMessage = ({ commonId: discussionMessage.commonId, directParent, onUserClick, + onStreamMentionClick, onFeedItemClick, onInternalLinkClick, }); diff --git a/src/shared/components/Chat/ChatMessage/DMChatMessage.tsx b/src/shared/components/Chat/ChatMessage/DMChatMessage.tsx index 99f8c2affa..8863bc506a 100644 --- a/src/shared/components/Chat/ChatMessage/DMChatMessage.tsx +++ b/src/shared/components/Chat/ChatMessage/DMChatMessage.tsx @@ -76,6 +76,7 @@ interface ChatMessageProps { onMessageDelete?: (messageId: string) => void; directParent?: DirectParent | null; onUserClick?: (userId: string) => void; + onStreamMentionClick?: (feedItemId: string) => void; onFeedItemClick?: (feedItemId: string) => void; onInternalLinkClick?: (data: InternalLinkData) => void; chatChannelId?: string; @@ -112,6 +113,7 @@ export default function DMChatMessage({ onMessageDelete, directParent, onUserClick, + onStreamMentionClick, onFeedItemClick, onInternalLinkClick, chatChannelId, @@ -181,6 +183,7 @@ export default function DMChatMessage({ getCommonPageAboutTabPath, directParent, onUserClick, + onStreamMentionClick, onFeedItemClick, onInternalLinkClick, }); @@ -201,6 +204,7 @@ export default function DMChatMessage({ getCommonPagePath, getCommonPageAboutTabPath, onUserClick, + onStreamMentionClick, ]); useEffect(() => { @@ -217,6 +221,7 @@ export default function DMChatMessage({ commonId: discussionMessage.commonId, directParent, onUserClick, + onStreamMentionClick, onFeedItemClick, onInternalLinkClick, }); @@ -229,6 +234,7 @@ export default function DMChatMessage({ isNotCurrentUserMessage, discussionMessage.commonId, onUserClick, + onStreamMentionClick, discussionMessageUserId, userId, onInternalLinkClick, diff --git a/src/shared/components/Chat/ChatMessage/components/StreamMention/StreamMention.tsx b/src/shared/components/Chat/ChatMessage/components/StreamMention/StreamMention.tsx new file mode 100644 index 0000000000..9fd2d74142 --- /dev/null +++ b/src/shared/components/Chat/ChatMessage/components/StreamMention/StreamMention.tsx @@ -0,0 +1,46 @@ +import React, { FC, useMemo } from "react"; +import classNames from "classnames"; +import styles from "../../ChatMessage.module.scss"; +import { useQuery } from "@tanstack/react-query"; +import { CommonFeedService } from "@/services"; + +interface StreamMentionProps { + commonId: string; + discussionId: string; + title: string; + mentionTextClassName?: string; + onStreamMentionClick?: (feedItemId: string) => void; +} + +const StreamMention: FC = (props) => { + const { discussionId, title, commonId, mentionTextClassName, onStreamMentionClick } = + props; + + const { data } = useQuery({ + queryKey: ["stream-mention", discussionId], + queryFn: () => CommonFeedService.getFeedItemByCommonAndDiscussionId({ commonId, discussionId }), + enabled: !!discussionId, + staleTime: Infinity + }) + + const feedItemId = useMemo(() => data?.id, [data?.id]); + + const handleStreamNameClick = () => { + if (onStreamMentionClick && feedItemId) { + onStreamMentionClick(feedItemId); + } + }; + + return ( + <> + + @{title} + + + ); +}; + +export default StreamMention; diff --git a/src/shared/components/Chat/ChatMessage/components/StreamMention/index.ts b/src/shared/components/Chat/ChatMessage/components/StreamMention/index.ts new file mode 100644 index 0000000000..eef391c277 --- /dev/null +++ b/src/shared/components/Chat/ChatMessage/components/StreamMention/index.ts @@ -0,0 +1 @@ +export { default as StreamMention } from "./StreamMention"; diff --git a/src/shared/components/Chat/ChatMessage/components/index.ts b/src/shared/components/Chat/ChatMessage/components/index.ts index 9c5e506643..5b72d65788 100644 --- a/src/shared/components/Chat/ChatMessage/components/index.ts +++ b/src/shared/components/Chat/ChatMessage/components/index.ts @@ -3,6 +3,7 @@ export * from "./CheckboxItem"; export * from "./MessageLinkPreview"; export * from "./Time"; export * from "./UserMention"; +export * from "./StreamMention"; export * from "./InternalLink"; export * from "./ReactWithEmoji"; export * from "./Reactions"; diff --git a/src/shared/components/Chat/ChatMessage/types.ts b/src/shared/components/Chat/ChatMessage/types.ts index 35d5b4d45e..0b812d28a3 100644 --- a/src/shared/components/Chat/ChatMessage/types.ts +++ b/src/shared/components/Chat/ChatMessage/types.ts @@ -18,6 +18,7 @@ export interface TextData { getCommonPageAboutTabPath?: GetCommonPageAboutTabPath; directParent?: DirectParent | null; onUserClick?: (userId: string) => void; + onStreamMentionClick?: (feedItemId: string) => void; onFeedItemClick?: (feedItemId: string) => void; onMessageUpdate?: (message: TextEditorValue) => void; onInternalLinkClick?: (data: InternalLinkData) => void; diff --git a/src/shared/components/Chat/ChatMessage/utils/getTextFromTextEditorString.tsx b/src/shared/components/Chat/ChatMessage/utils/getTextFromTextEditorString.tsx index a90ca84354..7f804576a7 100644 --- a/src/shared/components/Chat/ChatMessage/utils/getTextFromTextEditorString.tsx +++ b/src/shared/components/Chat/ChatMessage/utils/getTextFromTextEditorString.tsx @@ -13,7 +13,7 @@ import textEditorElementsStyles from "@/shared/ui-kit/TextEditor/shared/TextEdit import { EmojiElement } from "@/shared/ui-kit/TextEditor/types"; import { isRtlWithNoMentions } from "@/shared/ui-kit/TextEditor/utils"; import { InternalLinkData } from "@/shared/utils"; -import { CheckboxItem, UserMention } from "../components"; +import { CheckboxItem, StreamMention, UserMention } from "../components"; import { Text, TextData } from "../types"; import { generateInternalLink } from "./generateInternalLink"; import { getTextFromSystemMessage } from "./getTextFromSystemMessage"; @@ -35,6 +35,7 @@ interface TextFromDescendant { commonId?: string; directParent?: DirectParent | null; onUserClick?: (userId: string) => void; + onStreamMentionClick?: (feedItemId: string) => void; onInternalLinkClick?: (data: InternalLinkData) => void; showPlainText?: boolean; } @@ -47,6 +48,7 @@ const getTextFromDescendant = async ({ commonId, directParent, onUserClick, + onStreamMentionClick, onInternalLinkClick, showPlainText, }: TextFromDescendant): Promise => { @@ -60,7 +62,7 @@ const getTextFromDescendant = async ({ return await generateInternalLink({ text, onInternalLinkClick }); }), ); - return {mappedText} || ""; + return mappedText ? {mappedText} : ""; } switch (descendant.type) { @@ -79,6 +81,7 @@ const getTextFromDescendant = async ({ commonId, directParent, onUserClick, + onStreamMentionClick, onInternalLinkClick, showPlainText, })} @@ -98,6 +101,16 @@ const getTextFromDescendant = async ({ onUserClick={onUserClick} /> ); + case ElementType.StreamMention: + return ( + + ); case ElementType.Emoji: return ( void; + onStreamMentionClick?: (feedItemId: string) => void; onFeedItemClick?: (feedItemId: string) => void; users: User[]; textStyles: TextStyles; @@ -78,6 +79,7 @@ export const useDiscussionMessagesById = ({ discussionId, directParent, onUserClick, + onStreamMentionClick, onFeedItemClick, users, onInternalLinkClick, @@ -126,6 +128,7 @@ export const useDiscussionMessagesById = ({ getCommonPageAboutTabPath, directParent, onUserClick, + onStreamMentionClick: onStreamMentionClick ?? onFeedItemClick, onFeedItemClick, onInternalLinkClick, showPlainText: options?.showPlainText, @@ -196,6 +199,7 @@ export const useDiscussionMessagesById = ({ getCommonPageAboutTabPath, directParent, onUserClick, + onStreamMentionClick: onStreamMentionClick ?? onFeedItemClick, onFeedItemClick, onInternalLinkClick, }); @@ -228,6 +232,7 @@ export const useDiscussionMessagesById = ({ getCommonPagePath, getCommonPageAboutTabPath, onUserClick, + onStreamMentionClick, onFeedItemClick, onInternalLinkClick, ], @@ -293,6 +298,7 @@ export const useDiscussionMessagesById = ({ getCommonPageAboutTabPath, directParent, onUserClick, + onStreamMentionClick: onStreamMentionClick ?? onFeedItemClick, onFeedItemClick, onInternalLinkClick, }); @@ -339,6 +345,7 @@ export const useDiscussionMessagesById = ({ getCommonPagePath, getCommonPageAboutTabPath, onUserClick, + onStreamMentionClick, onFeedItemClick, onInternalLinkClick, dispatch, diff --git a/src/shared/hooks/useCases/usePreloadDiscussionMessagesById.ts b/src/shared/hooks/useCases/usePreloadDiscussionMessagesById.ts index 2fba3af495..1c04567eeb 100644 --- a/src/shared/hooks/useCases/usePreloadDiscussionMessagesById.ts +++ b/src/shared/hooks/useCases/usePreloadDiscussionMessagesById.ts @@ -16,6 +16,7 @@ interface Options { discussionId?: string | null; commonId?: string; onUserClick?: (userId: string) => void; + onStreamMentionClick?: (feedItemId: string) => void; onFeedItemClick?: (feedItemId: string) => void; onInternalLinkClick?: (data: InternalLinkData) => void; } @@ -31,6 +32,7 @@ export const usePreloadDiscussionMessagesById = ({ discussionId, commonId, onUserClick, + onStreamMentionClick, onFeedItemClick, onInternalLinkClick, }: Options): Return => { @@ -84,6 +86,7 @@ export const usePreloadDiscussionMessagesById = ({ getCommonPagePath, getCommonPageAboutTabPath, onUserClick, + onStreamMentionClick, onFeedItemClick, onInternalLinkClick, }); diff --git a/src/shared/hooks/useFetchDiscussionsByCommonId.tsx b/src/shared/hooks/useFetchDiscussionsByCommonId.tsx new file mode 100644 index 0000000000..cb8f490dcc --- /dev/null +++ b/src/shared/hooks/useFetchDiscussionsByCommonId.tsx @@ -0,0 +1,13 @@ +import { DiscussionService } from "@/services"; +import { useQuery } from "@tanstack/react-query"; + +// React Query hook to fetch discussions +export const useFetchDiscussionsByCommonId = (commonId: string) => { + return useQuery( + ["allDiscussion", commonId], // queryKey based on commonId + () => DiscussionService.getDiscussionsByCommonId(commonId), // Query function that calls Firestore + { + cacheTime: 5 * 60 * 1000, // Cache time set to 5 minutes (300,000 milliseconds) + } + ); + }; \ No newline at end of file diff --git a/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx b/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx index 87e1841587..beb4abad3b 100644 --- a/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx +++ b/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx @@ -25,7 +25,7 @@ import { withHistory } from "slate-history"; import { ReactEditor, Slate, withReact } from "slate-react"; import { DOMRange } from "slate-react/dist/utils/dom"; import { KeyboardKeys } from "@/shared/constants/keyboardKeys"; -import { User } from "@/shared/models"; +import { Discussion, User } from "@/shared/models"; import { getUserName, isMobile, isRtlText } from "@/shared/utils"; import { Editor, @@ -40,6 +40,7 @@ import { parseStringToTextEditorValue, insertEmoji, insertMention, + insertStreamMention, checkIsCheckboxCreationText, toggleCheckboxItem, checkIsEmptyCheckboxCreationText, @@ -73,6 +74,7 @@ export interface TextEditorProps { disabled?: boolean; onKeyDown?: (event: KeyboardEvent) => void; users?: User[]; + discussions?: Discussion[]; shouldReinitializeEditor: boolean; onClearFinished: () => void; scrollSelectionIntoView?: (editor: ReactEditor, domRange: DOMRange) => void; @@ -111,6 +113,7 @@ const BaseTextEditor = forwardRef((props onClearFinished, scrollSelectionIntoView, elementStyles, + discussions, } = props; const editor = useMemo( () => @@ -251,6 +254,12 @@ const BaseTextEditor = forwardRef((props .startsWith(search.text.substring(1).toLowerCase()); }); + const discussionChars = (discussions ?? []).filter((discussion) => { + return discussion.title + ?.toLowerCase() + .startsWith(search.text.substring(1).toLowerCase()); + }); + useEffect(() => { if (search && search.text) { setTarget({ @@ -409,7 +418,14 @@ const BaseTextEditor = forwardRef((props setTarget(null); setShouldFocusTarget(false); }} + onClickDiscussion={(discussion: Discussion) => { + Transforms.select(editor, target); + insertStreamMention(editor, discussion); + setTarget(null); + setShouldFocusTarget(false); + }} users={chars} + discussions={discussionChars} onClose={() => { setTarget(null); setShouldFocusTarget(false); diff --git a/src/shared/ui-kit/TextEditor/components/Element/Element.tsx b/src/shared/ui-kit/TextEditor/components/Element/Element.tsx index 17f1e7960b..bddabac37d 100644 --- a/src/shared/ui-kit/TextEditor/components/Element/Element.tsx +++ b/src/shared/ui-kit/TextEditor/components/Element/Element.tsx @@ -22,6 +22,20 @@ const Mention = ({ attributes, element, className, children }) => { ); }; +const StreamMention = ({ attributes, element, className, children }) => { + return ( + + @{element.title} + {children} + + ); +}; + const Element: FC = ( props, ) => { @@ -83,6 +97,14 @@ const Element: FC = ( /> ); } + case ElementType.StreamMention: { + return ( + + ) + } case ElementType.Emoji: { return ( void; + onClickDiscussion: (discussion: Discussion) => void; + discussions?: Discussion[]; onClose: () => void; users?: User[]; shouldFocusTarget?: boolean; @@ -20,7 +22,9 @@ export interface MentionDropdownProps { const MentionDropdown: FC = (props) => { const { onClick, + onClickDiscussion, users = [], + discussions = [], onClose, shouldFocusTarget, } = props; @@ -30,11 +34,12 @@ const MentionDropdown: FC = (props) => { const [index, setIndex] = useState(0); const userIds = useMemo(() => users.map(({ uid }) => uid), [users]); + const discussionIds = useMemo(() => discussions.map(({ id }) => id), [discussions]); useEffect(() => { if (shouldFocusTarget) { const filteredListRefs = uniq(listRefs.current).filter((item) => { - if (userIds.includes(item?.id)) { + if (userIds.includes(item?.id) || discussionIds.includes(item?.id)) { return true; } @@ -44,12 +49,14 @@ const MentionDropdown: FC = (props) => { listRefs.current = filteredListRefs; filteredListRefs && filteredListRefs?.[index]?.focus(); } - }, [index, shouldFocusTarget, userIds]); + }, [index, shouldFocusTarget, userIds, discussionIds]); const increment = () => { setIndex((value) => { const updatedValue = value + 1; - return updatedValue > users.length - 1 ? value : updatedValue; + const usersLastIndex = users.length - 1; + const discussionsLastIndex = discussions.length - 1; + return updatedValue > discussionsLastIndex + usersLastIndex ? value : updatedValue; }); }; const decrement = () => @@ -77,7 +84,11 @@ const MentionDropdown: FC = (props) => { break; } case KeyboardKeys.Enter: { - onClick(users[index]); + if(index > users.length - 1) { + onClickDiscussion(discussions[index - users.length]); + } else { + onClick(users[index]); + } } } }; @@ -92,7 +103,7 @@ const MentionDropdown: FC = (props) => { data-cy="mentions-portal" onKeyDown={onKeyDown} > - {users.length === 0 && ( + {(users.length === 0 && discussions.length === 0) && (
  • @@ -114,6 +125,23 @@ const MentionDropdown: FC = (props) => {

    {getUserName(user)}

    ))} + {discussions.map((discussion, index) => ( +
  • onClickDiscussion(discussion)} + className={styles.content} + > + +

    {discussion.title}

    +
  • + ))} ); }; diff --git a/src/shared/ui-kit/TextEditor/constants/elementType.ts b/src/shared/ui-kit/TextEditor/constants/elementType.ts index e9550961f5..45b5bb01c6 100644 --- a/src/shared/ui-kit/TextEditor/constants/elementType.ts +++ b/src/shared/ui-kit/TextEditor/constants/elementType.ts @@ -3,6 +3,7 @@ export enum ElementType { Heading = "heading", Link = "link", Mention = "mention", + StreamMention = "StreamMention", NumberedList = "numbered-list", BulletedList = "bulleted-list", ListItem = "list-item", @@ -19,5 +20,6 @@ export const PARENT_TYPES = [ export const INLINE_TYPES = [ ElementType.Link, ElementType.Mention, + ElementType.StreamMention, ElementType.Emoji, ]; diff --git a/src/shared/ui-kit/TextEditor/hofs/withMentions.ts b/src/shared/ui-kit/TextEditor/hofs/withMentions.ts index 91bc505b39..1e8522b921 100644 --- a/src/shared/ui-kit/TextEditor/hofs/withMentions.ts +++ b/src/shared/ui-kit/TextEditor/hofs/withMentions.ts @@ -9,14 +9,14 @@ export const withMentions = (editor: Editor): Editor => { checkIsInlineType(element.type) || isInline(element); editor.isVoid = (element) => { - return (element.type as ElementType) === ElementType.Mention + return ((element.type as ElementType) === ElementType.Mention || (element.type as ElementType) === ElementType.StreamMention) ? true : isVoid(element); }; editor.markableVoid = (element) => { return ( - (element.type as ElementType) === ElementType.Mention || + ((element.type as ElementType) === ElementType.Mention || (element.type as ElementType) === ElementType.StreamMention) || markableVoid(element) ); }; diff --git a/src/shared/ui-kit/TextEditor/types.ts b/src/shared/ui-kit/TextEditor/types.ts index 0386b74bd3..c8587e93d4 100644 --- a/src/shared/ui-kit/TextEditor/types.ts +++ b/src/shared/ui-kit/TextEditor/types.ts @@ -59,6 +59,13 @@ export interface MentionElement extends BaseElement { userId: string; } +export interface StreamMentionElement extends BaseElement { + type: ElementType.StreamMention; + title: string; + commonId: string; + discussionId +} + export interface EmojiElement extends BaseElement { type: ElementType.Emoji; emoji: Skin; @@ -90,5 +97,6 @@ export type CustomElement = | BulletedListElement | ListItemElement | MentionElement + | StreamMentionElement | EmojiElement | CheckboxItemElement; diff --git a/src/shared/ui-kit/TextEditor/utils/checkIsTextEditorValueEmpty.ts b/src/shared/ui-kit/TextEditor/utils/checkIsTextEditorValueEmpty.ts index 3d0f5e9537..c92ce30e33 100644 --- a/src/shared/ui-kit/TextEditor/utils/checkIsTextEditorValueEmpty.ts +++ b/src/shared/ui-kit/TextEditor/utils/checkIsTextEditorValueEmpty.ts @@ -18,7 +18,7 @@ export const checkIsTextEditorValueEmpty = ( const firstChild = firstElement.children[0]; const secondChild = firstElement.children[1]; - if (Element.isElementType(secondChild, ElementType.Mention)) { + if (Element.isElementType(secondChild, ElementType.Mention) || Element.isElementType(secondChild, ElementType.StreamMention)) { return false; } diff --git a/src/shared/ui-kit/TextEditor/utils/index.ts b/src/shared/ui-kit/TextEditor/utils/index.ts index 152bee9588..c8eb1b7130 100644 --- a/src/shared/ui-kit/TextEditor/utils/index.ts +++ b/src/shared/ui-kit/TextEditor/utils/index.ts @@ -29,4 +29,5 @@ export * from "./removeTextEditorEmptyEndLinesValues"; export * from "./countTextEditorEmojiElements"; export * from "./insertEmoji"; export * from "./insertMention"; +export * from "./insertStreamMention"; export * from "./isRtlWithNoMentions"; diff --git a/src/shared/ui-kit/TextEditor/utils/insertStreamMention.ts b/src/shared/ui-kit/TextEditor/utils/insertStreamMention.ts new file mode 100644 index 0000000000..3fabb6c9f6 --- /dev/null +++ b/src/shared/ui-kit/TextEditor/utils/insertStreamMention.ts @@ -0,0 +1,18 @@ +import { Transforms } from "slate"; +import { ReactEditor } from "slate-react"; +import { ElementType } from "../constants"; +import { StreamMentionElement } from "../types"; + +export const insertStreamMention = (editor, character) => { + const mention: StreamMentionElement = { + type: ElementType.StreamMention, + title: `${character.title} `, + commonId: character.commonId, + discussionId: character.id, + children: [{ text: "" }], + }; + Transforms.insertNodes(editor, mention); + Transforms.move(editor); + + ReactEditor.focus(editor); +}; diff --git a/src/shared/ui-kit/TextEditor/utils/removeTextEditorEmptyEndLinesValues.ts b/src/shared/ui-kit/TextEditor/utils/removeTextEditorEmptyEndLinesValues.ts index 1010d568d0..965df2661b 100644 --- a/src/shared/ui-kit/TextEditor/utils/removeTextEditorEmptyEndLinesValues.ts +++ b/src/shared/ui-kit/TextEditor/utils/removeTextEditorEmptyEndLinesValues.ts @@ -24,6 +24,7 @@ export const removeTextEditorEmptyEndLinesValues = ( if ( firstChild?.text !== "" || Element.isElementType(secondChild, ElementType.Mention) || + Element.isElementType(secondChild, ElementType.StreamMention) || Element.isElementType(secondChild, ElementType.Emoji) ) { endOfTextIndex = index; diff --git a/yarn.lock b/yarn.lock index 14ceec5b07..09680b41c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5097,6 +5097,19 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" +"@tanstack/query-core@4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.5.0.tgz#eeb0f290adb34f8682f65ebfce4e74709f3ae130" + integrity sha512-9pHE4TNlnBxdF24bTH3GGAJ4JdIDfJyuE/q+snyV425XEimPDe+OfofM8mVHfrn01Spvk9xAMpbqoEcmQG4kMg== + +"@tanstack/react-query@4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.5.0.tgz#566fbf4286a075d74cce32859ecfaafd11cb2d89" + integrity sha512-58JRis0+1hdKe37L7ZAJex849mlqhBvpNwlOjz6KzEMXHH/b0AyUHp1YIqn6ULiw7YpZiheYpCkdB/7ArIgfrg== + dependencies: + "@tanstack/query-core" "4.5.0" + use-sync-external-store "^1.2.0" + "@tanstack/react-table@^8.7.9": version "8.7.9" resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.7.9.tgz#9efcd168fb0080a7e0bc213b5eac8b55513babf4" @@ -19926,6 +19939,11 @@ use-long-press@^2.0.2: resolved "https://registry.yarnpkg.com/use-long-press/-/use-long-press-2.0.2.tgz#3c945ee45b671e9c6976fe5364bdb5f563b3ff82" integrity sha512-zQ4sujilCykA7fSZ+m2gDuGw5aW3Gm3M4pulRH4e8c4mGXw8MDQIMthCsHiolxpt/hCe/BbIvd/iDn9XNDzkYg== +use-sync-external-store@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" + integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"