From 56b9da48401218a0ab5a8d5fbd7c9933be3c7651 Mon Sep 17 00:00:00 2001
From: Pavel Meyer
Date: Sat, 2 Nov 2024 01:52:15 +0300
Subject: [PATCH 01/12] 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"
From 5d58c1f8874b08e6d26dfa35d4e51bc04294a62c Mon Sep 17 00:00:00 2001
From: Pavel Meyer
Date: Tue, 26 Nov 2024 15:17:53 +0300
Subject: [PATCH 02/12] CW-mention-streams
Fixed UI.
Fixed control of dropdown
---
src/shared/icons/chat.icon.tsx | 26 +
src/shared/icons/dots.icon.tsx | 35 +
src/shared/icons/index.ts | 2 +
.../ui-kit/TextEditor/BaseTextEditor.tsx | 669 +++++++++---------
.../MentionDropdown.module.scss | 24 +-
.../MentionDropdown/MentionDropdown.tsx | 182 ++++-
6 files changed, 579 insertions(+), 359 deletions(-)
create mode 100644 src/shared/icons/chat.icon.tsx
create mode 100644 src/shared/icons/dots.icon.tsx
diff --git a/src/shared/icons/chat.icon.tsx b/src/shared/icons/chat.icon.tsx
new file mode 100644
index 0000000000..69cf4c7de0
--- /dev/null
+++ b/src/shared/icons/chat.icon.tsx
@@ -0,0 +1,26 @@
+import React, { FC } from "react";
+
+interface ChatIconProps {
+ className?: string;
+ size?: number;
+}
+
+export const ChatIcon: FC = ({ className, size = 20 }) => {
+ return (
+
+ );
+};
diff --git a/src/shared/icons/dots.icon.tsx b/src/shared/icons/dots.icon.tsx
new file mode 100644
index 0000000000..fed8f78d24
--- /dev/null
+++ b/src/shared/icons/dots.icon.tsx
@@ -0,0 +1,35 @@
+import React, { ReactElement } from "react";
+
+interface DotsIconProps {
+ className?: string;
+ size?: number;
+}
+
+export function DotsIcon({
+ className,
+ size = 24,
+}: DotsIconProps): ReactElement {
+ return (
+
+ );
+}
diff --git a/src/shared/icons/index.ts b/src/shared/icons/index.ts
index 20cb914a19..b08d2945ec 100644
--- a/src/shared/icons/index.ts
+++ b/src/shared/icons/index.ts
@@ -79,3 +79,5 @@ export { default as HideIcon } from "./hide.icon";
export { default as InboxFilterIcon } from "./inboxFilter.icon";
export { default as GroupChatIcon } from "./groupChat.icon";
export { default as NotificationsIcon } from "./notifications.icon";
+export * from "./chat.icon";
+export * from "./dots.icon";
diff --git a/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx b/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx
index beb4abad3b..bfa45ee87a 100644
--- a/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx
+++ b/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx
@@ -24,7 +24,9 @@ import {
import { withHistory } from "slate-history";
import { ReactEditor, Slate, withReact } from "slate-react";
import { DOMRange } from "slate-react/dist/utils/dom";
+import { AI_PRO_USER, AI_USER, FeatureFlags } from "@/shared/constants";
import { KeyboardKeys } from "@/shared/constants/keyboardKeys";
+import { useFeatureFlag } from "@/shared/hooks";
import { Discussion, User } from "@/shared/models";
import { getUserName, isMobile, isRtlText } from "@/shared/utils";
import {
@@ -46,8 +48,6 @@ import {
checkIsEmptyCheckboxCreationText,
} from "./utils";
import styles from "./BaseTextEditor.module.scss";
-import { useFeatureFlag } from "@/shared/hooks";
-import { AI_PRO_USER, AI_USER, FeatureFlags } from "@/shared/constants";
export interface BaseTextEditorHandles {
focus: () => void;
@@ -60,8 +60,8 @@ export interface TextEditorProps {
emojiContainerClassName?: string;
emojiPickerContainerClassName?: string;
inputContainerRef?:
- | MutableRefObject
- | RefCallback;
+ | MutableRefObject
+ | RefCallback;
editorRef?: MutableRefObject | RefCallback;
id?: string;
name?: string;
@@ -91,350 +91,361 @@ const INITIAL_SEARCH_VALUE = {
},
};
-const BaseTextEditor = forwardRef((props, ref) => {
- const {
- className,
- classNameRtl,
- emojiContainerClassName,
- emojiPickerContainerClassName,
- editorRef,
- inputContainerRef,
- id,
- name,
- value,
- onChange,
- onBlur,
- onKeyDown,
- size,
- placeholder,
- disabled = false,
- users,
- shouldReinitializeEditor = false,
- onClearFinished,
- scrollSelectionIntoView,
- elementStyles,
- discussions,
- } = props;
- const editor = useMemo(
- () =>
- withChecklists(
- withEmojis(
- withMentions(
- withInlines(withHistory(withReact(createEditor())), {
- shouldInsertURLAsLink: false,
- }),
+const BaseTextEditor = forwardRef(
+ (props, ref) => {
+ const {
+ className,
+ classNameRtl,
+ emojiContainerClassName,
+ emojiPickerContainerClassName,
+ editorRef,
+ inputContainerRef,
+ id,
+ name,
+ value,
+ onChange,
+ onBlur,
+ onKeyDown,
+ size,
+ placeholder,
+ disabled = false,
+ users,
+ shouldReinitializeEditor = false,
+ onClearFinished,
+ scrollSelectionIntoView,
+ elementStyles,
+ discussions,
+ } = props;
+ const editor = useMemo(
+ () =>
+ withChecklists(
+ withEmojis(
+ withMentions(
+ withInlines(withHistory(withReact(createEditor())), {
+ shouldInsertURLAsLink: false,
+ }),
+ ),
),
),
- ),
- [],
- );
- const featureFlags = useFeatureFlag();
- const isAiBotProEnabled = featureFlags?.get(FeatureFlags.AiBotPro);
-
- const usersWithAI = useMemo(
- () => [isAiBotProEnabled ? AI_PRO_USER : AI_USER, ...(users ?? [])],
- [users],
- );
-
- const [target, setTarget] = useState();
- const [shouldFocusTarget, setShouldFocusTarget] = useState(false);
-
- const [isRtlLanguage, setIsRtlLanguage] = useState(false);
-
- useDebounce(
- () => {
- setIsRtlLanguage(isRtlText(EditorSlate.string(editor, [])));
- },
- 5000,
- [value],
- );
-
- const clearInput = () => {
- setTimeout(() => {
- Transforms.delete(editor, {
- at: {
- anchor: EditorSlate.start(editor, []),
- focus: EditorSlate.end(editor, []),
- },
- });
+ [],
+ );
+ const featureFlags = useFeatureFlag();
+ const isAiBotProEnabled = featureFlags?.get(FeatureFlags.AiBotPro);
+
+ const usersWithAI = useMemo(
+ () => [isAiBotProEnabled ? AI_PRO_USER : AI_USER, ...(users ?? [])],
+ [users],
+ );
+
+ const [target, setTarget] = useState();
+ const [shouldFocusTarget, setShouldFocusTarget] = useState(false);
+
+ const [isRtlLanguage, setIsRtlLanguage] = useState(false);
+
+ useDebounce(
+ () => {
+ setIsRtlLanguage(isRtlText(EditorSlate.string(editor, [])));
+ },
+ 5000,
+ [value],
+ );
+
+ const clearInput = () => {
+ setTimeout(() => {
+ Transforms.delete(editor, {
+ at: {
+ anchor: EditorSlate.start(editor, []),
+ focus: EditorSlate.end(editor, []),
+ },
+ });
- // Removes empty node
- Transforms.removeNodes(editor, {
- at: [0],
- });
+ // Removes empty node
+ Transforms.removeNodes(editor, {
+ at: [0],
+ });
- // Insert array of children nodes
- Transforms.insertNodes(editor, parseStringToTextEditorValue());
- const editorEl = ReactEditor.toDOMNode(editor, editor);
- editorEl.scrollTo(0, 0);
- onClearFinished();
- }, 0)
- }
-
- useEffect(() => {
- if (!shouldReinitializeEditor) {
- return;
- }
-
- clearInput();
- }, [shouldReinitializeEditor, clearInput]);
-
- useImperativeHandle(ref, () => ({
- focus: () => {
- if (editorRef) {
- const end = EditorSlate.end(editor, []);
-
- // Move the selection to the end
- Transforms.select(editor, end);
-
- // Focus the editor DOM node
+ // Insert array of children nodes
+ Transforms.insertNodes(editor, parseStringToTextEditorValue());
const editorEl = ReactEditor.toDOMNode(editor, editor);
- editorEl.focus();
-
- // Ensure the editor itself is focused programmatically
- ReactEditor.focus(editor);
- }
- },
- clear: () => {
- clearInput();
- }
- }));
-
- useEffect(() => {
- if (!editorRef) {
- return;
- }
-
- const editorEl = ReactEditor.toDOMNode(editor, editor);
-
- if (typeof editorRef === "function") {
- editorRef(editorEl);
- } else {
- editorRef.current = editorEl;
- }
- }, [editorRef, editor]);
-
- const [search, setSearch] = useState(INITIAL_SEARCH_VALUE);
-
- const handleSearch = (text: string, value?: BaseRange) => {
- if (!value || !value?.anchor || !text || text === "") {
- setSearch(INITIAL_SEARCH_VALUE);
- setTarget(null);
- setShouldFocusTarget(false);
- return;
- }
-
- if (text === MENTION_TAG) {
- setSearch({
- text,
- ...value.anchor,
- range: value,
- });
- } else if (text.match(/^(\s|$)/)) {
- setSearch(INITIAL_SEARCH_VALUE);
- setTarget(null);
- setShouldFocusTarget(false);
- } else if (
- search.text.includes(MENTION_TAG) &&
- isEqual(search.path, value.anchor.path) &&
- search.offset + 1 === value.anchor.offset
- ) {
- setSearch({
- ...search,
- text: search.text + text,
- ...value.anchor,
- });
- setShouldFocusTarget(false);
- }
- };
-
- const chars = (usersWithAI ?? []).filter((user) => {
- return getUserName(user)
- ?.toLowerCase()
- .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({
- ...search.range,
- focus: {
- ...search.range.focus,
- offset: search.range.focus.offset + search.text.length - 1,
- },
- });
- }
- }, [search]);
-
- const [isMessageSent, setIsMessageSent] = useState(false);
-
- const onToggleIsMessageSent = () => {
- setIsMessageSent((value) => !value);
- };
-
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.key === KeyboardKeys.ArrowUp && target) {
- event.preventDefault();
- setShouldFocusTarget(true);
- } else {
- // event.stopPropagation();
- onKeyDown && onKeyDown(event); // Call any custom onKeyDown handler
- if (event.key === KeyboardKeys.Enter && !isMobile()) {
- onToggleIsMessageSent();
+ editorEl.scrollTo(0, 0);
+ onClearFinished();
+ }, 0);
+ };
+
+ useEffect(() => {
+ if (!shouldReinitializeEditor) {
+ return;
}
- }
- };
-
- const handleOnChangeSelection = (selection: BaseSelection) => {
- if (selection && Range.isCollapsed(selection)) {
- const {
- anchor: { path: selectionPath, offset: selectionOffset },
- } = selection;
- const [start] = Range.edges(selection);
- const before = EditorSlate.before(editor, start);
- const lineLastPoint = EditorSlate.after(
- editor,
- {
- anchor: {
- offset: 0,
- path: selectionPath,
- },
- focus: {
- offset: 0,
- path: selectionPath,
- },
- },
- { unit: "line" },
- );
- const beforeRange = before && EditorSlate.range(editor, before, start);
- const beforeText = beforeRange && EditorSlate.string(editor, beforeRange);
- const checkboxText = EditorSlate.string(editor, {
- anchor: { offset: 0, path: selectionPath },
- focus: { offset: 3, path: selectionPath },
- }).trim();
- if (
- beforeText === " " &&
- (selectionOffset === 3 || selectionOffset === 4) &&
- selectionOffset === lineLastPoint?.offset &&
- checkIsCheckboxCreationText(checkboxText)
- ) {
- toggleCheckboxItem(
- editor,
- !checkIsEmptyCheckboxCreationText(checkboxText),
- true,
- );
+ clearInput();
+ }, [shouldReinitializeEditor, clearInput]);
+
+ useImperativeHandle(ref, () => ({
+ focus: () => {
+ if (editorRef) {
+ const end = EditorSlate.end(editor, []);
+
+ // Move the selection to the end
+ Transforms.select(editor, end);
+
+ // Focus the editor DOM node
+ const editorEl = ReactEditor.toDOMNode(editor, editor);
+ editorEl.focus();
+
+ // Ensure the editor itself is focused programmatically
+ ReactEditor.focus(editor);
+ }
+ },
+ clear: () => {
+ clearInput();
+ },
+ }));
+
+ useEffect(() => {
+ if (!editorRef) {
return;
}
- handleSearch(beforeText ?? "", beforeRange);
- }
- };
+ const editorEl = ReactEditor.toDOMNode(editor, editor);
+
+ if (typeof editorRef === "function") {
+ editorRef(editorEl);
+ } else {
+ editorRef.current = editorEl;
+ }
+ }, [editorRef, editor]);
+
+ const [search, setSearch] = useState(INITIAL_SEARCH_VALUE);
- const handleMentionSelectionChange = useCallback(() => {
- if (!editor.selection || editor.selection.anchor.path.length <= 2) {
- return;
- }
+ const handleSearch = (text: string, value?: BaseRange) => {
+ if (!value || !value?.anchor || !text || text === "") {
+ setSearch(INITIAL_SEARCH_VALUE);
+ setTarget(null);
+ setShouldFocusTarget(false);
+ return;
+ }
- const { anchor } = editor.selection;
- const point: BaseRange["anchor"] = {
- ...anchor,
- path: [anchor.path[0], anchor.path[1] + 1],
+ if (text === MENTION_TAG) {
+ setSearch({
+ text,
+ ...value.anchor,
+ range: value,
+ });
+ } else if (text.match(/^(\s|$)/)) {
+ setSearch(INITIAL_SEARCH_VALUE);
+ setTarget(null);
+ setShouldFocusTarget(false);
+ } else if (
+ search.text.includes(MENTION_TAG) &&
+ isEqual(search.path, value.anchor.path) &&
+ search.offset + 1 === value.anchor.offset
+ ) {
+ setSearch({
+ ...search,
+ text: search.text + text,
+ ...value.anchor,
+ });
+ setShouldFocusTarget(false);
+ }
};
- Transforms.select(editor, {
- anchor: point,
- focus: point,
+
+ const chars = (usersWithAI ?? []).filter((user) => {
+ return getUserName(user)
+ ?.toLowerCase()
+ .startsWith(search.text.substring(1).toLowerCase());
+ });
+
+ const discussionChars = (discussions ?? []).filter((discussion) => {
+ return discussion.title
+ ?.toLowerCase()
+ .startsWith(search.text.substring(1).toLowerCase());
});
- }, []);
- const handleOnChange = useCallback(
- (updatedContent) => {
- // Prevent update for cursor clicks
- if (isEqual(updatedContent, value)) {
- handleMentionSelectionChange();
+ useEffect(() => {
+ if (search && search.text) {
+ setTarget({
+ ...search.range,
+ focus: {
+ ...search.range.focus,
+ offset: search.range.focus.offset + search.text.length - 1,
+ },
+ });
+ }
+ }, [search]);
+
+ const [isMessageSent, setIsMessageSent] = useState(false);
+
+ const onToggleIsMessageSent = () => {
+ setIsMessageSent((value) => !value);
+ };
+
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === KeyboardKeys.ArrowUp && target) {
+ event.preventDefault();
+ setShouldFocusTarget(true);
+ } else {
+ // event.stopPropagation();
+ onKeyDown && onKeyDown(event); // Call any custom onKeyDown handler
+ if (event.key === KeyboardKeys.Enter && !isMobile()) {
+ onToggleIsMessageSent();
+ }
+ }
+ };
+
+ const handleOnChangeSelection = (selection: BaseSelection) => {
+ if (selection && Range.isCollapsed(selection)) {
+ const {
+ anchor: { path: selectionPath, offset: selectionOffset },
+ } = selection;
+ const [start] = Range.edges(selection);
+ const before = EditorSlate.before(editor, start);
+ const lineLastPoint = EditorSlate.after(
+ editor,
+ {
+ anchor: {
+ offset: 0,
+ path: selectionPath,
+ },
+ focus: {
+ offset: 0,
+ path: selectionPath,
+ },
+ },
+ { unit: "line" },
+ );
+ const beforeRange = before && EditorSlate.range(editor, before, start);
+ const beforeText =
+ beforeRange && EditorSlate.string(editor, beforeRange);
+ const checkboxText = EditorSlate.string(editor, {
+ anchor: { offset: 0, path: selectionPath },
+ focus: { offset: 3, path: selectionPath },
+ }).trim();
+
+ if (
+ beforeText === " " &&
+ (selectionOffset === 3 || selectionOffset === 4) &&
+ selectionOffset === lineLastPoint?.offset &&
+ checkIsCheckboxCreationText(checkboxText)
+ ) {
+ toggleCheckboxItem(
+ editor,
+ !checkIsEmptyCheckboxCreationText(checkboxText),
+ true,
+ );
+ return;
+ }
+
+ handleSearch(beforeText ?? "", beforeRange);
+ }
+ };
+
+ const handleMentionSelectionChange = useCallback(() => {
+ if (!editor.selection || editor.selection.anchor.path.length <= 2) {
return;
}
- onChange && onChange(updatedContent);
- const { selection } = editor;
-
- handleOnChangeSelection(selection);
- },
- [onChange, value, handleMentionSelectionChange],
- );
-
- const customScrollSelectionIntoView = ( ) => {
- if (inputContainerRef && 'current' in inputContainerRef && inputContainerRef?.current) {
- inputContainerRef.current?.scrollIntoView({
- behavior: "smooth",
- block: "end",
- inline: "nearest",
+
+ const { anchor } = editor.selection;
+ const point: BaseRange["anchor"] = {
+ ...anchor,
+ path: [anchor.path[0], anchor.path[1] + 1],
+ };
+ Transforms.select(editor, {
+ anchor: point,
+ focus: point,
});
- }
- }
-
- return (
-
-
-
- {
- insertEmoji(editor, emoji.native);
- }}
- />
-
- {target && (
- {
- Transforms.select(editor, target);
- insertMention(editor, user);
- 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);
+ }, []);
+
+ const handleOnChange = useCallback(
+ (updatedContent) => {
+ // Prevent update for cursor clicks
+ if (isEqual(updatedContent, value)) {
+ handleMentionSelectionChange();
+ return;
+ }
+ onChange && onChange(updatedContent);
+ const { selection } = editor;
+
+ handleOnChangeSelection(selection);
+ },
+ [onChange, value, handleMentionSelectionChange],
+ );
+
+ const customScrollSelectionIntoView = () => {
+ if (
+ inputContainerRef &&
+ "current" in inputContainerRef &&
+ inputContainerRef?.current
+ ) {
+ inputContainerRef.current?.scrollIntoView({
+ behavior: "smooth",
+ block: "end",
+ inline: "nearest",
+ });
+ }
+ };
+
+ return (
+
+
+
+ {
+ insertEmoji(editor, emoji.native);
}}
/>
- )}
-
-
- );
-});
+
+ {target && (
+ {
+ Transforms.select(editor, target);
+ insertMention(editor, user);
+ setTarget(null);
+ setShouldFocusTarget(false);
+ }}
+ onClickDiscussion={(discussion: Discussion) => {
+ Transforms.select(editor, target);
+ insertStreamMention(editor, discussion);
+ setTarget(null);
+ setShouldFocusTarget(false);
+ }}
+ users={chars}
+ discussions={discussionChars}
+ initUsers={usersWithAI}
+ initDiscussions={discussions}
+ onClose={() => {
+ setTarget(null);
+ setShouldFocusTarget(false);
+ }}
+ />
+ )}
+
+
+ );
+ },
+);
export default BaseTextEditor;
diff --git a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.module.scss b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.module.scss
index 80ece806b6..7b147c01da 100644
--- a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.module.scss
+++ b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.module.scss
@@ -4,7 +4,7 @@
position: absolute;
bottom: 5.25rem;
left: 2.875rem;
- width: 20.125rem;
+ width: 25.125rem;
max-height: 22.75rem;
background: var(--tertiary-fill);
color: var(--primary-text);
@@ -22,7 +22,6 @@
align-items: center;
width: 100%;
box-sizing: border-box;
- border-bottom: 0.0625rem solid var(--gentle-stroke);
&:hover {
cursor: pointer;
@@ -35,13 +34,34 @@
}
}
+.sectionTitle {
+ font-weight: 500;
+ font-size: $mobile-title;
+ padding: 0.5rem 1rem;
+ margin: 0.25rem 0;
+}
+
+.separator {
+ margin: 1rem 0rem 0.5rem;
+ height: 2px;
+}
+
.userAvatar {
margin-right: 0.5625rem;
+ width: 1.75rem;
+ height: 1.75rem;
+}
+
+.streamIcon {
+ margin-right: 0.5625rem;
+ width: 1.5rem;
+ height: 1.5rem;
}
.userName {
font-weight: 500;
font-size: $mobile-title;
+ margin: 0.25rem 0;
}
.loader {
diff --git a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
index 44730f35f2..bf5d0ccd3a 100644
--- a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
+++ b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
@@ -1,8 +1,10 @@
import React, { FC, useEffect, useMemo, useRef, useState } from "react";
import { uniq } from "lodash";
-import { UserAvatar } from "@/shared/components";
+import { Separator, UserAvatar } from "@/shared/components";
import { KeyboardKeys } from "@/shared/constants/keyboardKeys";
import { useOutsideClick } from "@/shared/hooks";
+import { ChatIcon } from "@/shared/icons";
+import { DotsIcon } from "@/shared/icons";
import { Discussion, User } from "@/shared/models";
import { Loader } from "@/shared/ui-kit";
import { getUserName } from "@/shared/utils";
@@ -13,12 +15,19 @@ export const MENTION_TAG = "@";
export interface MentionDropdownProps {
onClick: (user: User) => void;
onClickDiscussion: (discussion: Discussion) => void;
- discussions?: Discussion[];
onClose: () => void;
users?: User[];
+ discussions?: Discussion[];
+ initUsers?: User[];
+ initDiscussions?: Discussion[];
shouldFocusTarget?: boolean;
}
+type CombinedItem =
+ | User
+ | Discussion
+ | { type: "toggleUsers" | "toggleDiscussions" | "separator" };
+
const MentionDropdown: FC = (props) => {
const {
onClick,
@@ -27,43 +36,71 @@ const MentionDropdown: FC = (props) => {
discussions = [],
onClose,
shouldFocusTarget,
+ initUsers = [],
+ initDiscussions = [],
} = props;
const mentionRef = useRef(null);
const listRefs = useRef([]);
const { isOutside, setOutsideValue } = useOutsideClick(mentionRef);
const [index, setIndex] = useState(0);
+ const [isShowMoreUsers, setIsShowMoreUsers] = useState(false);
+ const [isShowMoreDiscussions, setIsShowMoreDiscussions] = useState(false);
+ const usersList = useMemo(() => {
+ if (isShowMoreUsers) {
+ return users;
+ }
+
+ return users.slice(0, 5);
+ }, [isShowMoreUsers, users]);
- const userIds = useMemo(() => users.map(({ uid }) => uid), [users]);
- const discussionIds = useMemo(() => discussions.map(({ id }) => id), [discussions]);
+ const discussionsList = useMemo(() => {
+ if (isShowMoreDiscussions) {
+ return discussions;
+ }
+
+ return discussions.slice(0, 5);
+ }, [isShowMoreDiscussions, discussions]);
useEffect(() => {
if (shouldFocusTarget) {
- const filteredListRefs = uniq(listRefs.current).filter((item) => {
- if (userIds.includes(item?.id) || discussionIds.includes(item?.id)) {
- return true;
- }
+ // Clear and rebuild listRefs based on current usersList and discussionsList
+ listRefs.current = [];
+ const allRefs = document.querySelectorAll(
+ `.${styles.content}`,
+ );
+ allRefs.forEach((ref) => listRefs.current.push(ref));
- return false;
+ // Sort the listRefs by tabIndex
+ listRefs.current.sort((a, b) => {
+ const tabIndexA = parseInt(a.getAttribute("tabIndex") || "0", 10);
+ const tabIndexB = parseInt(b.getAttribute("tabIndex") || "0", 10);
+ return tabIndexA - tabIndexB;
});
- listRefs.current = filteredListRefs;
- filteredListRefs && filteredListRefs?.[index]?.focus();
+ // Find the element with the matching tabIndex and focus it
+ const elementToFocus = listRefs.current.find(
+ (item) => parseInt(item.getAttribute("tabIndex") || "0", 10) === index,
+ );
+
+ if (elementToFocus) {
+ elementToFocus.focus();
+ }
}
- }, [index, shouldFocusTarget, userIds, discussionIds]);
+ }, [index, usersList, discussionsList, shouldFocusTarget]);
const increment = () => {
setIndex((value) => {
const updatedValue = value + 1;
- const usersLastIndex = users.length - 1;
- const discussionsLastIndex = discussions.length - 1;
- return updatedValue > discussionsLastIndex + usersLastIndex ? value : updatedValue;
+ return updatedValue < listRefs.current.length ? updatedValue : value;
});
};
- const decrement = () =>
+
+ const decrement = () => {
setIndex((value) => {
const updatedValue = value - 1;
return updatedValue >= 0 ? updatedValue : value;
});
+ };
useEffect(() => {
if (isOutside) {
@@ -84,16 +121,59 @@ const MentionDropdown: FC = (props) => {
break;
}
case KeyboardKeys.Enter: {
- if(index > users.length - 1) {
- onClickDiscussion(discussions[index - users.length]);
- } else {
+ const currentElement = listRefs.current.find(
+ (item) =>
+ parseInt(item.getAttribute("tabIndex") || "0", 10) === index,
+ );
+
+ if (!currentElement) return;
+
+ const type = currentElement.dataset.type;
+
+ if (type === "user") {
+ // Handle user selection
onClick(users[index]);
+ } else if (type === "discussion") {
+ // Handle discussion selection
+ const discussionIndex =
+ index - usersList.length - (users.length > 5 ? 1 : 0);
+ onClickDiscussion(discussions[discussionIndex]);
+ } else if (type === "toggleUsers") {
+ // Toggle "Show More Users"
+ setIsShowMoreUsers((prev) => {
+ if (!prev && users.length > 5) {
+ // If expanding, move focus to the 6th user
+ setIndex(5);
+ } else {
+ // If collapsing, move focus back to the toggleUsers button
+ setIndex(5);
+ }
+ return !prev;
+ });
+ } else if (type === "toggleDiscussions") {
+ // Toggle "Show More Discussions"
+ setIsShowMoreDiscussions((prev) => {
+ if (!prev && discussions.length > 5) {
+ // If expanding, move focus to the 6th discussion
+ setIndex(usersList.length + 5);
+ } else {
+ // If collapsing, move focus back to the toggleDiscussions button
+ setIndex(usersList.length + 5);
+ }
+ return !prev;
+ });
}
+ break;
}
}
};
const getRef = (element) => listRefs.current.push(element);
+ const isEmptyContent =
+ (initUsers.length > 0 || initDiscussions.length > 0) &&
+ users.length === 0 &&
+ discussions.length === 0;
+ const isLoading = initUsers.length === 0 && initDiscussions.length === 0;
return (
);
};
From 915c3869b5ade73d3f851079d4f4ea7fa7761eaa Mon Sep 17 00:00:00 2001
From: Pavel Meyer
Date: Fri, 6 Dec 2024 15:18:39 +0300
Subject: [PATCH 03/12] CW-mention-streams
Added support multiline and spaces
Fixed UI for mention
---
.../ChatComponent/ChatComponent.tsx | 9 +-
.../components/ChatInput/ChatInput.tsx | 189 ++++++++++--------
src/shared/constants/keyboardKeys.ts | 1 +
.../ui-kit/TextEditor/BaseTextEditor.tsx | 138 ++++++++++---
.../MentionDropdown.module.scss | 5 +-
.../MentionDropdown/MentionDropdown.tsx | 132 +++++++++---
6 files changed, 332 insertions(+), 142 deletions(-)
diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx
index bed47c5079..28c3a1a3c8 100644
--- a/src/pages/common/components/ChatComponent/ChatComponent.tsx
+++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx
@@ -55,6 +55,7 @@ import {
removeTextEditorEmptyEndLinesValues,
countTextEditorEmojiElements,
} from "@/shared/ui-kit";
+import { BaseTextEditorHandles } from "@/shared/ui-kit/TextEditor/BaseTextEditor";
import { checkUncheckedItemsInTextEditorValue } from "@/shared/ui-kit/TextEditor/utils";
import { InternalLinkData, notEmpty } from "@/shared/utils";
import { getUserName, hasPermission, isMobile } from "@/shared/utils";
@@ -85,7 +86,6 @@ import {
uploadFilesAndImages,
} from "./utils";
import styles from "./ChatComponent.module.scss";
-import { BaseTextEditorHandles } from "@/shared/ui-kit/TextEditor/BaseTextEditor";
const BASE_CHAT_INPUT_HEIGHT = 48;
@@ -220,7 +220,7 @@ export default function ChatComponent({
participants: chatChannel?.participants,
});
- const {data: discussionsData} = useFetchDiscussionsByCommonId(commonId);
+ const { data: discussionsData } = useFetchDiscussionsByCommonId(commonId);
const users = useMemo(
() => (chatChannel ? chatUsers : discussionUsers),
@@ -731,7 +731,7 @@ export default function ChatComponent({
useLayoutEffect(() => {
textInputRef?.current?.clear?.();
textInputRef?.current?.focus?.();
- },[discussionId]);
+ }, [discussionId]);
useEffect(() => {
if (isFetchedDiscussionMessages) {
@@ -888,6 +888,9 @@ export default function ChatComponent({
canSendMessage={Boolean(canSendMessage)}
inputContainerRef={inputContainerRef}
editorRef={editorRef}
+ user={user}
+ commonId={commonId}
+ circleVisibility={discussion?.circleVisibility}
/>
diff --git a/src/pages/common/components/ChatComponent/components/ChatInput/ChatInput.tsx b/src/pages/common/components/ChatComponent/components/ChatInput/ChatInput.tsx
index f80f128a58..e0311c13eb 100644
--- a/src/pages/common/components/ChatComponent/components/ChatInput/ChatInput.tsx
+++ b/src/pages/common/components/ChatComponent/components/ChatInput/ChatInput.tsx
@@ -16,10 +16,10 @@ import {
TextEditorSize,
TextEditorValue,
} from "@/shared/ui-kit";
+import { BaseTextEditorHandles } from "@/shared/ui-kit/TextEditor/BaseTextEditor";
import { EmojiCount } from "@/shared/ui-kit/TextEditor/utils";
import { emptyFunction } from "@/shared/utils";
import styles from "./ChatInput.module.scss";
-import { BaseTextEditorHandles } from "@/shared/ui-kit/TextEditor/BaseTextEditor";
interface ChatInputProps {
shouldHideChatInput: boolean;
@@ -36,98 +36,111 @@ interface ChatInputProps {
canSendMessage?: boolean;
sendChatMessage: () => void;
inputContainerRef?:
- | MutableRefObject
- | RefCallback;
+ | MutableRefObject
+ | RefCallback;
editorRef?: MutableRefObject | RefCallback;
renderChatInputOuter?: () => ReactElement;
isAuthorized?: boolean;
+ circleVisibility?: string[];
+ user?: User | null;
+ commonId?: string;
}
-export const ChatInput = React.memo(forwardRef((props, ref): ReactElement | null => {
- const {
- inputContainerRef,
- editorRef,
- canSendMessage,
- sendChatMessage,
- shouldHideChatInput,
- isChatChannel,
- renderChatInputOuter,
- isAuthorized,
- uploadFiles,
- message,
- setMessage,
- emojiCount,
- onEnterKeyDown,
- users,
- discussions,
- shouldReinitializeEditor,
- onClearFinished,
- } = props;
+export const ChatInput = React.memo(
+ forwardRef(
+ (props, ref): ReactElement | null => {
+ const {
+ inputContainerRef,
+ editorRef,
+ canSendMessage,
+ sendChatMessage,
+ shouldHideChatInput,
+ isChatChannel,
+ renderChatInputOuter,
+ isAuthorized,
+ uploadFiles,
+ message,
+ setMessage,
+ emojiCount,
+ onEnterKeyDown,
+ users,
+ discussions,
+ shouldReinitializeEditor,
+ onClearFinished,
+ circleVisibility,
+ user,
+ commonId,
+ } = props;
- if (shouldHideChatInput) {
- return null;
- }
- if (!isChatChannel) {
- const chatInputEl = renderChatInputOuter?.();
+ if (shouldHideChatInput) {
+ return null;
+ }
+ if (!isChatChannel) {
+ const chatInputEl = renderChatInputOuter?.();
- if (chatInputEl || chatInputEl === null) {
- return chatInputEl;
- }
- }
- if (!isAuthorized) {
- return null;
- }
+ if (chatInputEl || chatInputEl === null) {
+ return chatInputEl;
+ }
+ }
+ if (!isAuthorized) {
+ return null;
+ }
- return (
- <>
- {
- document.getElementById("file")?.click();
- }}
- >
-
-
-
-
-
- >
- );
-}));
+ return (
+ <>
+ {
+ document.getElementById("file")?.click();
+ }}
+ >
+
+
+
+
+
+ >
+ );
+ },
+ ),
+);
diff --git a/src/shared/constants/keyboardKeys.ts b/src/shared/constants/keyboardKeys.ts
index 54fea94a6c..503f8bf134 100644
--- a/src/shared/constants/keyboardKeys.ts
+++ b/src/shared/constants/keyboardKeys.ts
@@ -3,6 +3,7 @@ export enum KeyboardKeys {
Escape = "Escape",
ArrowUp = "ArrowUp",
ArrowDown = "ArrowDown",
+ Backspace = "Backspace", // Add Backspace
}
/**
diff --git a/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx b/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx
index bfa45ee87a..59f5593ea9 100644
--- a/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx
+++ b/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx
@@ -1,4 +1,5 @@
import React, {
+ useRef,
FocusEventHandler,
KeyboardEvent,
MutableRefObject,
@@ -20,6 +21,8 @@ import {
Editor as EditorSlate,
BaseRange,
BaseSelection,
+ BasePoint,
+ Text,
} from "slate";
import { withHistory } from "slate-history";
import { ReactEditor, Slate, withReact } from "slate-react";
@@ -79,6 +82,9 @@ export interface TextEditorProps {
onClearFinished: () => void;
scrollSelectionIntoView?: (editor: ReactEditor, domRange: DOMRange) => void;
elementStyles?: EditorElementStyles;
+ circleVisibility?: string[];
+ user?: User | null;
+ commonId?: string;
}
const INITIAL_SEARCH_VALUE = {
@@ -115,6 +121,9 @@ const BaseTextEditor = forwardRef(
scrollSelectionIntoView,
elementStyles,
discussions,
+ circleVisibility,
+ user,
+ commonId,
} = props;
const editor = useMemo(
() =>
@@ -139,6 +148,7 @@ const BaseTextEditor = forwardRef(
const [target, setTarget] = useState();
const [shouldFocusTarget, setShouldFocusTarget] = useState(false);
+ const isNewMentionCreated = useRef(false);
const [isRtlLanguage, setIsRtlLanguage] = useState(false);
@@ -217,33 +227,89 @@ const BaseTextEditor = forwardRef(
const [search, setSearch] = useState(INITIAL_SEARCH_VALUE);
- const handleSearch = (text: string, value?: BaseRange) => {
- if (!value || !value?.anchor || !text || text === "") {
+ const getTextByRange = (editor, range) => {
+ if (
+ !range ||
+ !EditorSlate.hasPath(editor, range.anchor.path) ||
+ !EditorSlate.hasPath(editor, range.focus.path)
+ ) {
+ return "";
+ }
+
+ const filteredNodes = Array.from(
+ EditorSlate.nodes(editor, { at: range }),
+ ).filter(([node]) => Text.isText(node));
+ const nodes = filteredNodes.map(([node, path]) => {
+ // Determine the start and end offsets for this node
+ const { anchor, focus } =
+ Range.intersection(range, EditorSlate.range(editor, path)) || {};
+
+ if (!anchor || !focus) return "";
+
+ // Extract the substring within the offsets
+ const text =
+ (node as Text)?.text.slice(anchor.offset, focus.offset) +
+ (filteredNodes.length > 1 ? " " : "");
+
+ // Remove newlines from the text
+ return text.replace(/\n/g, "");
+ });
+
+ // Combine the text parts and return
+ return nodes.join("").slice(0, -1);
+ };
+
+ const handleSearch = (
+ text: string,
+ value?: BaseRange,
+ afterValue?: BasePoint,
+ ) => {
+ if (!value || !value?.anchor || isNewMentionCreated.current) {
setSearch(INITIAL_SEARCH_VALUE);
+ isNewMentionCreated.current = false;
setTarget(null);
setShouldFocusTarget(false);
return;
}
+ const newText = target?.anchor
+ ? getTextByRange(editor, {
+ ...target,
+ focus: afterValue ? afterValue : value.anchor,
+ })
+ : "";
+
if (text === MENTION_TAG) {
setSearch({
text,
...value.anchor,
range: value,
});
- } else if (text.match(/^(\s|$)/)) {
- setSearch(INITIAL_SEARCH_VALUE);
- setTarget(null);
- setShouldFocusTarget(false);
- } else if (
- search.text.includes(MENTION_TAG) &&
- isEqual(search.path, value.anchor.path) &&
- search.offset + 1 === value.anchor.offset
- ) {
- setSearch({
- ...search,
- text: search.text + text,
- ...value.anchor,
+ } else if (search.text.includes(MENTION_TAG) && text.match(/^\s+/)) {
+ setSearch((prevSearch) => {
+ {
+ return {
+ ...prevSearch,
+ text: newText,
+ ...value.anchor,
+ range: {
+ ...prevSearch.range,
+ focus: afterValue ? afterValue : value.anchor,
+ },
+ };
+ }
+ });
+ } else if (search.text.includes(MENTION_TAG)) {
+ setSearch((prevSearch) => {
+ return {
+ ...prevSearch,
+ text: newText,
+ ...value.anchor,
+ range: {
+ ...prevSearch.range,
+ focus: afterValue ? afterValue : value.anchor,
+ },
+ };
});
setShouldFocusTarget(false);
}
@@ -267,7 +333,7 @@ const BaseTextEditor = forwardRef(
...search.range,
focus: {
...search.range.focus,
- offset: search.range.focus.offset + search.text.length - 1,
+ offset: search.range.focus.offset + 1,
},
});
}
@@ -283,8 +349,21 @@ const BaseTextEditor = forwardRef(
if (event.key === KeyboardKeys.ArrowUp && target) {
event.preventDefault();
setShouldFocusTarget(true);
+ } else if (
+ event.key === KeyboardKeys.Enter &&
+ !event.shiftKey &&
+ target
+ ) {
+ setSearch(INITIAL_SEARCH_VALUE);
+ setTarget(null);
+ setShouldFocusTarget(false);
+ isNewMentionCreated.current = true;
+ } else if (event.key === KeyboardKeys.Escape && target) {
+ event.preventDefault();
+ setSearch(INITIAL_SEARCH_VALUE);
+ setTarget(null);
+ setShouldFocusTarget(false);
} else {
- // event.stopPropagation();
onKeyDown && onKeyDown(event); // Call any custom onKeyDown handler
if (event.key === KeyboardKeys.Enter && !isMobile()) {
onToggleIsMessageSent();
@@ -335,11 +414,11 @@ const BaseTextEditor = forwardRef(
return;
}
- handleSearch(beforeText ?? "", beforeRange);
+ handleSearch(beforeText ?? "", beforeRange, lineLastPoint);
}
};
- const handleMentionSelectionChange = useCallback(() => {
+ const handleMentionSelectionChange = () => {
if (!editor.selection || editor.selection.anchor.path.length <= 2) {
return;
}
@@ -353,21 +432,18 @@ const BaseTextEditor = forwardRef(
anchor: point,
focus: point,
});
- }, []);
+ };
const handleOnChange = useCallback(
(updatedContent) => {
- // Prevent update for cursor clicks
if (isEqual(updatedContent, value)) {
handleMentionSelectionChange();
return;
}
onChange && onChange(updatedContent);
- const { selection } = editor;
-
- handleOnChangeSelection(selection);
+ handleOnChangeSelection(editor.selection);
},
- [onChange, value, handleMentionSelectionChange],
+ [onChange, value, handleSearch, handleMentionSelectionChange],
);
const customScrollSelectionIntoView = () => {
@@ -425,18 +501,30 @@ const BaseTextEditor = forwardRef(
insertMention(editor, user);
setTarget(null);
setShouldFocusTarget(false);
+ isNewMentionCreated.current = true;
}}
onClickDiscussion={(discussion: Discussion) => {
Transforms.select(editor, target);
insertStreamMention(editor, discussion);
setTarget(null);
setShouldFocusTarget(false);
+ isNewMentionCreated.current = true;
+ }}
+ onCreateDiscussion={(discussionId) => {
+ console.log("--discussionId", discussionId);
+ Transforms.select(editor, target);
+ Transforms.insertText(editor, "Hello, Slate!");
}}
+ user={user}
+ commonId={commonId}
+ circleVisibility={circleVisibility}
users={chars}
discussions={discussionChars}
initUsers={usersWithAI}
initDiscussions={discussions}
+ searchText={search.text}
onClose={() => {
+ setSearch(INITIAL_SEARCH_VALUE);
setTarget(null);
setShouldFocusTarget(false);
}}
diff --git a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.module.scss b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.module.scss
index 7b147c01da..8660414c22 100644
--- a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.module.scss
+++ b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.module.scss
@@ -4,8 +4,9 @@
position: absolute;
bottom: 5.25rem;
left: 2.875rem;
- width: 25.125rem;
- max-height: 22.75rem;
+ width: 34.375rem;
+ min-height: 25rem;
+ max-height: 25rem;
background: var(--tertiary-fill);
color: var(--primary-text);
border-radius: 0.25rem;
diff --git a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
index bf5d0ccd3a..628770d6b5 100644
--- a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
+++ b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
@@ -1,13 +1,20 @@
import React, { FC, useEffect, useMemo, useRef, useState } from "react";
-import { uniq } from "lodash";
+import { useDispatch } from "react-redux";
+import { v4 as uuidv4 } from "uuid";
import { Separator, UserAvatar } from "@/shared/components";
+import { DiscussionMessageOwnerType } from "@/shared/constants";
import { KeyboardKeys } from "@/shared/constants/keyboardKeys";
import { useOutsideClick } from "@/shared/hooks";
-import { ChatIcon } from "@/shared/icons";
+import { ChatIcon, PlusIcon } from "@/shared/icons";
import { DotsIcon } from "@/shared/icons";
-import { Discussion, User } from "@/shared/models";
+import { CommonFeedType, Discussion, User } from "@/shared/models";
import { Loader } from "@/shared/ui-kit";
-import { getUserName } from "@/shared/utils";
+import {
+ generateFirstMessage,
+ generateOptimisticFeedItem,
+ getUserName,
+} from "@/shared/utils";
+import { commonActions } from "@/store/states";
import styles from "./MentionDropdown.module.scss";
export const MENTION_TAG = "@";
@@ -15,33 +22,50 @@ export const MENTION_TAG = "@";
export interface MentionDropdownProps {
onClick: (user: User) => void;
onClickDiscussion: (discussion: Discussion) => void;
+ onCreateDiscussion: (discussionId: string) => void;
onClose: () => void;
users?: User[];
discussions?: Discussion[];
initUsers?: User[];
initDiscussions?: Discussion[];
shouldFocusTarget?: boolean;
+ searchText: string;
+ circleVisibility?: string[];
+ user?: User | null;
+ commonId?: string;
}
-type CombinedItem =
- | User
- | Discussion
- | { type: "toggleUsers" | "toggleDiscussions" | "separator" };
-
const MentionDropdown: FC = (props) => {
const {
onClick,
onClickDiscussion,
+ onCreateDiscussion,
users = [],
discussions = [],
onClose,
shouldFocusTarget,
initUsers = [],
initDiscussions = [],
+ searchText = "",
+ circleVisibility,
+ user,
+ commonId,
} = props;
const mentionRef = useRef(null);
const listRefs = useRef([]);
const { isOutside, setOutsideValue } = useOutsideClick(mentionRef);
+ const dispatch = useDispatch();
+
+ const canCreateDiscussion = !!user && !!commonId;
+ console.log(
+ "--user",
+ user,
+ "--commonId",
+ commonId,
+ "--circleVisibility",
+ circleVisibility,
+ );
+
const [index, setIndex] = useState(0);
const [isShowMoreUsers, setIsShowMoreUsers] = useState(false);
const [isShowMoreDiscussions, setIsShowMoreDiscussions] = useState(false);
@@ -82,6 +106,7 @@ const MentionDropdown: FC = (props) => {
(item) => parseInt(item.getAttribute("tabIndex") || "0", 10) === index,
);
+ console.log("---elementToFocus", elementToFocus, index);
if (elementToFocus) {
elementToFocus.focus();
}
@@ -91,6 +116,7 @@ const MentionDropdown: FC = (props) => {
const increment = () => {
setIndex((value) => {
const updatedValue = value + 1;
+ console.log("--listRefs", listRefs, updatedValue);
return updatedValue < listRefs.current.length ? updatedValue : value;
});
};
@@ -162,12 +188,60 @@ const MentionDropdown: FC = (props) => {
}
return !prev;
});
+ } else if (type === "newDiscussion" && searchText) {
+ createDiscussion();
}
break;
}
}
};
+ const createDiscussion = () => {
+ if (!canCreateDiscussion) {
+ return;
+ }
+
+ const discussionId = uuidv4();
+ const userName = getUserName(user);
+ const userId = user.uid;
+ const firstMessage = generateFirstMessage({ userName, userId });
+ dispatch(
+ commonActions.setOptimisticFeedItem(
+ generateOptimisticFeedItem({
+ userId,
+ commonId,
+ type: CommonFeedType.OptimisticDiscussion,
+ circleVisibility: circleVisibility ?? [],
+ discussionId,
+ title: searchText,
+ content: firstMessage,
+ lastMessageContent: {
+ ownerId: userId,
+ userName,
+ ownerType: DiscussionMessageOwnerType.System,
+ content: firstMessage,
+ },
+ }),
+ ),
+ );
+
+ dispatch(
+ commonActions.createDiscussion.request({
+ payload: {
+ id: discussionId,
+ title: searchText,
+ message: firstMessage,
+ ownerId: userId,
+ commonId,
+ images: [],
+ circleVisibility: circleVisibility ?? [],
+ },
+ }),
+ );
+
+ onCreateDiscussion(discussionId);
+ };
+
const getRef = (element) => listRefs.current.push(element);
const isEmptyContent =
(initUsers.length > 0 || initDiscussions.length > 0) &&
@@ -212,7 +286,7 @@ const MentionDropdown: FC = (props) => {
{getUserName(user)}
))}
- {users.length > 5 && (
+ {users.length > 5 && !isShowMoreUsers && (
= (props) => {
className={styles.content}
>
- {isShowMoreUsers ? (
- Hide results
- ) : (
- {users.length - 5} more results
- )}
+ {users.length - 5} more results
)}
{users.length > 0 && discussions.length > 0 && (
)}
{discussions.length > 0 && (
- Link to Stream
+ Link to stream
)}
{discussionsList.map((discussion, index) => (
= (props) => {
{discussion.title}
))}
- {discussions.length > 5 && (
+ {discussions.length > 5 && !isShowMoreDiscussions && (
= (props) => {
className={styles.content}
>
- {isShowMoreDiscussions ? (
- Hide results
- ) : (
-
- {discussions.length - 5} more results
-
- )}
+
+ {discussions.length - 5} more results
+
)}
+ {/* {((users.length > 0 || discussions.length > 0) && canCreateDiscussion && searchText) && (
+
+ )}
+ {(searchText && canCreateDiscussion) && (
+ 0 ? usersList.length + 1 : 0) + (discussionsList.length > 0 ? discussionsList.length + 1 : 0)}
+ key="newDiscussion"
+ data-type="newDiscussion"
+ onClick={createDiscussion}
+ className={styles.content}
+ >
+
+
+ New "{searchText.slice(1)}" discussion
+
+
+ )} */}
);
};
From 6ef91db0d1477a1f1539f82fa61f71eb5d0fc9f4 Mon Sep 17 00:00:00 2001
From: Pavel Meyer
Date: Fri, 6 Dec 2024 15:40:57 +0300
Subject: [PATCH 04/12] Update MentionDropdown.tsx
---
.../MentionDropdown/MentionDropdown.tsx | 64 +++++++++----------
1 file changed, 32 insertions(+), 32 deletions(-)
diff --git a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
index 628770d6b5..a0f483bffb 100644
--- a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
+++ b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
@@ -205,39 +205,39 @@ const MentionDropdown: FC = (props) => {
const userName = getUserName(user);
const userId = user.uid;
const firstMessage = generateFirstMessage({ userName, userId });
- dispatch(
- commonActions.setOptimisticFeedItem(
- generateOptimisticFeedItem({
- userId,
- commonId,
- type: CommonFeedType.OptimisticDiscussion,
- circleVisibility: circleVisibility ?? [],
- discussionId,
- title: searchText,
- content: firstMessage,
- lastMessageContent: {
- ownerId: userId,
- userName,
- ownerType: DiscussionMessageOwnerType.System,
- content: firstMessage,
- },
- }),
- ),
- );
+ // dispatch(
+ // commonActions.setOptimisticFeedItem(
+ // generateOptimisticFeedItem({
+ // userId,
+ // commonId,
+ // type: CommonFeedType.OptimisticDiscussion,
+ // circleVisibility: circleVisibility ?? [],
+ // discussionId,
+ // title: searchText,
+ // content: firstMessage,
+ // lastMessageContent: {
+ // ownerId: userId,
+ // userName,
+ // ownerType: DiscussionMessageOwnerType.System,
+ // content: firstMessage,
+ // },
+ // }),
+ // ),
+ // );
- dispatch(
- commonActions.createDiscussion.request({
- payload: {
- id: discussionId,
- title: searchText,
- message: firstMessage,
- ownerId: userId,
- commonId,
- images: [],
- circleVisibility: circleVisibility ?? [],
- },
- }),
- );
+ // dispatch(
+ // commonActions.createDiscussion.request({
+ // payload: {
+ // id: discussionId,
+ // title: searchText,
+ // message: firstMessage,
+ // ownerId: userId,
+ // commonId,
+ // images: [],
+ // circleVisibility: circleVisibility ?? [],
+ // },
+ // }),
+ // );
onCreateDiscussion(discussionId);
};
From 3e8e3f8e6a379ea856a33bc3a7a0b17f20f9cd34 Mon Sep 17 00:00:00 2001
From: Pavel Meyer
Date: Mon, 9 Dec 2024 15:09:30 +0300
Subject: [PATCH 05/12] CW-mention-streams
Added new discussion creation
---
.../components/FeedLayout/FeedLayout.tsx | 14 +++
src/pages/inbox/BaseInbox.tsx | 11 ++-
.../utils/generateInternalLink.tsx | 16 ++-
src/shared/constants/queryParamKey.ts | 1 +
src/shared/models/CommonFeed.tsx | 1 +
.../ui-kit/TextEditor/BaseTextEditor.tsx | 23 +++--
.../MentionDropdown/MentionDropdown.tsx | 99 ++++++++++---------
.../utils/generateOptimisticFeedItem.ts | 3 +
src/shared/utils/generateStaticShareLink.ts | 13 ++-
9 files changed, 117 insertions(+), 64 deletions(-)
diff --git a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx
index 0ce7b452eb..62daad7df1 100644
--- a/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx
+++ b/src/pages/commonFeed/components/FeedLayout/FeedLayout.tsx
@@ -56,6 +56,7 @@ import {
ChatChannelFeedLayoutItemProps,
checkIsChatChannelLayoutItem,
checkIsFeedItemFollowLayoutItem,
+ FeedItemFollowLayoutItem,
FeedLayoutItem,
FeedLayoutItemChangeData,
FeedLayoutItemChangeDataWithType,
@@ -696,8 +697,21 @@ const FeedLayout: ForwardRefRenderFunction = (
}
const itemId = data.params[QueryParamKey.Item];
+ const discussionItemId = data.params[QueryParamKey.discussionItem];
const messageId = data.params[QueryParamKey.Message];
+ if(discussionItemId) {
+ const feedItem = allFeedItems.find((item) => (item as FeedItemFollowLayoutItem)?.feedItem?.data.id === discussionItemId);
+ const feedItemId = feedItem?.itemId;
+ if(feedItemId) {
+ handleFeedItemClick(feedItemId, {
+ commonId: feedPageParams.id,
+ });
+ }
+
+ return;
+ }
+
if (itemId) {
handleFeedItemClick(itemId, {
commonId: feedPageParams.id,
diff --git a/src/pages/inbox/BaseInbox.tsx b/src/pages/inbox/BaseInbox.tsx
index 711ba2868d..1a91479f6d 100644
--- a/src/pages/inbox/BaseInbox.tsx
+++ b/src/pages/inbox/BaseInbox.tsx
@@ -152,10 +152,13 @@ const InboxPage: FC = (props) => {
const firstFeedItem = topFeedItems[0];
if(optimisticInboxFeedItems.size > 0 && firstFeedItem) {
- feedLayoutRef?.setActiveItem({
- feedItemId: firstFeedItem.itemId,
- discussion: (firstFeedItem as FeedItemFollowLayoutItemWithFollowData)?.feedItem.optimisticData,
- });
+ const shouldFocus = (firstFeedItem as FeedItemFollowLayoutItemWithFollowData)?.feedItem.optimisticData?.shouldFocus;
+ if(shouldFocus) {
+ feedLayoutRef?.setActiveItem({
+ feedItemId: firstFeedItem.itemId,
+ discussion: (firstFeedItem as FeedItemFollowLayoutItemWithFollowData)?.feedItem.optimisticData,
+ });
+ }
}
}, [topFeedItems, optimisticInboxFeedItems, feedLayoutRef])
diff --git a/src/shared/components/Chat/ChatMessage/utils/generateInternalLink.tsx b/src/shared/components/Chat/ChatMessage/utils/generateInternalLink.tsx
index 8b53adf920..d6b7c992bc 100644
--- a/src/shared/components/Chat/ChatMessage/utils/generateInternalLink.tsx
+++ b/src/shared/components/Chat/ChatMessage/utils/generateInternalLink.tsx
@@ -16,6 +16,7 @@ interface GenerateInternalLinkProps {
}
const ITEM_KEY = "item";
+const DISCUSSION_ITEM_KEY = "discussionItem";
export const getQueryParam = (path: string, key: string): string | null => {
const urlParams = new URLSearchParams(path);
@@ -44,6 +45,16 @@ const getStreamNameByFeedItemId = async (
}
};
+const getDiscussionTitle = async (
+ discussionId: string,
+): Promise => {
+ const discussion = await DiscussionService.getDiscussionById(discussionId);
+
+ return discussion?.title;
+};
+
+
+
export const generateInternalLink = async ({
text,
onInternalLinkClick,
@@ -52,11 +63,12 @@ export const generateInternalLink = async ({
if (text.startsWith(BASE_URL) && commonPath) {
const [commonId, itemQueryParam] = commonPath.split("?");
const itemId = getQueryParam(itemQueryParam, ITEM_KEY);
+ const discussionItemId = getQueryParam(itemQueryParam, DISCUSSION_ITEM_KEY);
if (commonId) {
const common = await getCommon(commonId);
if (common?.id && common.name) {
- const itemTitle = await getStreamNameByFeedItemId(commonId, itemId);
-
+ const itemTitle = discussionItemId ? await getDiscussionTitle(discussionItemId) : await getStreamNameByFeedItemId(commonId, itemId);
+
return (
<>
{renderLink({
diff --git a/src/shared/constants/queryParamKey.ts b/src/shared/constants/queryParamKey.ts
index c98c135cfb..cf7a85fd09 100644
--- a/src/shared/constants/queryParamKey.ts
+++ b/src/shared/constants/queryParamKey.ts
@@ -5,6 +5,7 @@ export enum QueryParamKey {
Language = "language",
Tab = "tab",
Item = "item",
+ discussionItem = "discussionItem",
ChatItem = "chatItem",
Message = "message",
Unread = "unread",
diff --git a/src/shared/models/CommonFeed.tsx b/src/shared/models/CommonFeed.tsx
index ccbe1095b0..90acc456d2 100644
--- a/src/shared/models/CommonFeed.tsx
+++ b/src/shared/models/CommonFeed.tsx
@@ -39,6 +39,7 @@ export interface LastMessageContentWithMessageId {
export type DiscussionWithOptimisticData = Discussion & {
state?: OptimisticFeedItemState; // Optional state property
lastMessageContent: LastMessageContent; // Additional property
+ shouldFocus: boolean;
};
export interface CommonFeed extends BaseEntity, SoftDeleteEntity {
diff --git a/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx b/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx
index 59f5593ea9..eea340f3eb 100644
--- a/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx
+++ b/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx
@@ -31,7 +31,7 @@ import { AI_PRO_USER, AI_USER, FeatureFlags } from "@/shared/constants";
import { KeyboardKeys } from "@/shared/constants/keyboardKeys";
import { useFeatureFlag } from "@/shared/hooks";
import { Discussion, User } from "@/shared/models";
-import { getUserName, isMobile, isRtlText } from "@/shared/utils";
+import { generateDiscussionShareLink, getUserName, isMobile, isRtlText } from "@/shared/utils";
import {
Editor,
MentionDropdown,
@@ -248,15 +248,18 @@ const BaseTextEditor = forwardRef(
// Extract the substring within the offsets
const text =
- (node as Text)?.text.slice(anchor.offset, focus.offset) +
+ (node as Text)?.text.slice(anchor.offset, focus.offset + 1) +
(filteredNodes.length > 1 ? " " : "");
// Remove newlines from the text
return text.replace(/\n/g, "");
});
- // Combine the text parts and return
- return nodes.join("").slice(0, -1);
+ if(nodes.length > 1) {
+ return nodes.join("").slice(0, -1);
+ }
+
+ return nodes.join("");
};
const handleSearch = (
@@ -275,7 +278,7 @@ const BaseTextEditor = forwardRef(
const newText = target?.anchor
? getTextByRange(editor, {
...target,
- focus: afterValue ? afterValue : value.anchor,
+ focus: afterValue ? { ...afterValue, offset: afterValue.offset } : { ...value.anchor, offset: value.anchor.offset + 1 },
})
: "";
@@ -492,7 +495,6 @@ const BaseTextEditor = forwardRef(
insertEmoji(editor, emoji.native);
}}
/>
-
{target && (
(
setShouldFocusTarget(false);
isNewMentionCreated.current = true;
}}
- onCreateDiscussion={(discussionId) => {
- console.log("--discussionId", discussionId);
+ onCreateDiscussion={(createdDiscussionCommonId: string, discussionId: string) => {
Transforms.select(editor, target);
- Transforms.insertText(editor, "Hello, Slate!");
+ const link = generateDiscussionShareLink(createdDiscussionCommonId, discussionId);
+ Transforms.insertText(editor, link);
+ setTarget(null);
+ setShouldFocusTarget(false);
+ isNewMentionCreated.current = true;
}}
user={user}
commonId={commonId}
diff --git a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
index a0f483bffb..24e864ed41 100644
--- a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
+++ b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
@@ -14,15 +14,16 @@ import {
generateOptimisticFeedItem,
getUserName,
} from "@/shared/utils";
-import { commonActions } from "@/store/states";
+import { commonActions, optimisticActions } from "@/store/states";
import styles from "./MentionDropdown.module.scss";
+import { CommonService } from "@/services";
export const MENTION_TAG = "@";
export interface MentionDropdownProps {
onClick: (user: User) => void;
onClickDiscussion: (discussion: Discussion) => void;
- onCreateDiscussion: (discussionId: string) => void;
+ onCreateDiscussion: (createdDiscussionCommonId: string, discussionId: string) => void;
onClose: () => void;
users?: User[];
discussions?: Discussion[];
@@ -57,14 +58,6 @@ const MentionDropdown: FC = (props) => {
const dispatch = useDispatch();
const canCreateDiscussion = !!user && !!commonId;
- console.log(
- "--user",
- user,
- "--commonId",
- commonId,
- "--circleVisibility",
- circleVisibility,
- );
const [index, setIndex] = useState(0);
const [isShowMoreUsers, setIsShowMoreUsers] = useState(false);
@@ -106,7 +99,6 @@ const MentionDropdown: FC = (props) => {
(item) => parseInt(item.getAttribute("tabIndex") || "0", 10) === index,
);
- console.log("---elementToFocus", elementToFocus, index);
if (elementToFocus) {
elementToFocus.focus();
}
@@ -116,7 +108,6 @@ const MentionDropdown: FC = (props) => {
const increment = () => {
setIndex((value) => {
const updatedValue = value + 1;
- console.log("--listRefs", listRefs, updatedValue);
return updatedValue < listRefs.current.length ? updatedValue : value;
});
};
@@ -196,50 +187,62 @@ const MentionDropdown: FC = (props) => {
}
};
- const createDiscussion = () => {
+ const createDiscussion = async () => {
if (!canCreateDiscussion) {
return;
}
+ const common = await CommonService.getCachedCommonById(commonId);
+
+ if (!common) {
+ return;
+ }
+
const discussionId = uuidv4();
const userName = getUserName(user);
const userId = user.uid;
const firstMessage = generateFirstMessage({ userName, userId });
- // dispatch(
- // commonActions.setOptimisticFeedItem(
- // generateOptimisticFeedItem({
- // userId,
- // commonId,
- // type: CommonFeedType.OptimisticDiscussion,
- // circleVisibility: circleVisibility ?? [],
- // discussionId,
- // title: searchText,
- // content: firstMessage,
- // lastMessageContent: {
- // ownerId: userId,
- // userName,
- // ownerType: DiscussionMessageOwnerType.System,
- // content: firstMessage,
- // },
- // }),
- // ),
- // );
+ const optimisticFeedItem = generateOptimisticFeedItem({
+ userId,
+ commonId,
+ type: CommonFeedType.OptimisticDiscussion,
+ circleVisibility: circleVisibility ?? [],
+ discussionId,
+ title: searchText,
+ content: firstMessage,
+ lastMessageContent: {
+ ownerId: userId,
+ userName,
+ ownerType: DiscussionMessageOwnerType.System,
+ content: firstMessage,
+ },
+ shouldFocus: false
+ });
+ dispatch(
+ optimisticActions.setOptimisticFeedItem({
+ data: optimisticFeedItem,
+ common
+ }),
+ );
- // dispatch(
- // commonActions.createDiscussion.request({
- // payload: {
- // id: discussionId,
- // title: searchText,
- // message: firstMessage,
- // ownerId: userId,
- // commonId,
- // images: [],
- // circleVisibility: circleVisibility ?? [],
- // },
- // }),
- // );
+ console.log('---discussionId',discussionId, '--optimisticFeedItem',optimisticFeedItem.id);
- onCreateDiscussion(discussionId);
+ dispatch(
+ commonActions.createDiscussion.request({
+ payload: {
+ id: discussionId,
+ title: searchText,
+ message: firstMessage,
+ ownerId: userId,
+ commonId,
+ images: [],
+ circleVisibility: circleVisibility ?? [],
+ },
+ commonId
+ }),
+ );
+
+ onCreateDiscussion(commonId, discussionId);
};
const getRef = (element) => listRefs.current.push(element);
@@ -334,7 +337,7 @@ const MentionDropdown: FC = (props) => {
)}
- {/* {((users.length > 0 || discussions.length > 0) && canCreateDiscussion && searchText) && (
+ {((users.length > 0 || discussions.length > 0) && canCreateDiscussion && searchText) && (
)}
{(searchText && canCreateDiscussion) && (
@@ -351,7 +354,7 @@ const MentionDropdown: FC = (props) => {
New "{searchText.slice(1)}" discussion
- )} */}
+ )}
);
};
diff --git a/src/shared/utils/generateOptimisticFeedItem.ts b/src/shared/utils/generateOptimisticFeedItem.ts
index 18ce7a2e51..570b9ade51 100644
--- a/src/shared/utils/generateOptimisticFeedItem.ts
+++ b/src/shared/utils/generateOptimisticFeedItem.ts
@@ -11,6 +11,7 @@ interface GenerateOptimisticFeedItemPayload {
content: string;
circleVisibility: string[];
lastMessageContent: LastMessageContent
+ shouldFocus?: boolean;
}
export const generateOptimisticFeedItem = ({
@@ -22,6 +23,7 @@ export const generateOptimisticFeedItem = ({
content,
circleVisibility,
lastMessageContent,
+ shouldFocus = true
}: GenerateOptimisticFeedItemPayload): CommonFeed => {
const optimisticFeedItemId = uuidv4();
@@ -60,6 +62,7 @@ export const generateOptimisticFeedItem = ({
circleVisibilityByCommon: null,
linkedCommonIds: [],
state: OptimisticFeedItemState.loading,
+ shouldFocus: shouldFocus
},
circleVisibility,
}
diff --git a/src/shared/utils/generateStaticShareLink.ts b/src/shared/utils/generateStaticShareLink.ts
index c68737288f..f5a153b56d 100644
--- a/src/shared/utils/generateStaticShareLink.ts
+++ b/src/shared/utils/generateStaticShareLink.ts
@@ -2,7 +2,7 @@ import { Environment, REACT_APP_ENV, ROUTE_PATHS } from "../constants";
import { Common, Discussion, DiscussionMessage, Proposal } from "../models";
import { matchRoute } from "./matchRoute";
-const staticLinkPrefix = () => {
+export const staticLinkPrefix = () => {
if (window.location.hostname === "localhost") {
return "http://localhost:3000";
}
@@ -71,3 +71,14 @@ export const generateStaticShareLink = (
return "";
}
};
+
+export const generateDiscussionShareLink = (
+ commonId: string,
+ discussionId: string,
+): string => {
+ const basePath: string = getStaticLinkBasePath();
+
+ return `${staticLinkPrefix()}/${basePath}/${
+ commonId
+ }?discussionItem=${discussionId}`;
+};
\ No newline at end of file
From 5106e0bc66305cc869f57143e13a6021c083ea0a Mon Sep 17 00:00:00 2001
From: Pavel Meyer
Date: Mon, 9 Dec 2024 15:31:31 +0300
Subject: [PATCH 06/12] CW-mention-streams
Fixed keyboard for create new
---
.../MentionDropdown.module.scss | 19 ++++++++
.../MentionDropdown/MentionDropdown.tsx | 47 ++++++++++++++++---
2 files changed, 60 insertions(+), 6 deletions(-)
diff --git a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.module.scss b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.module.scss
index 8660414c22..89b0d24ece 100644
--- a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.module.scss
+++ b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.module.scss
@@ -35,6 +35,25 @@
}
}
+.emptyContent {
+ padding: 0.375rem 1.75rem;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ width: 100%;
+ box-sizing: border-box;
+
+ &:hover {
+ cursor: pointer;
+ background-color: var(--hover-fill);
+ }
+
+ &:focus {
+ background-color: var(--hover-fill);
+ outline: none;
+ }
+}
+
.sectionTitle {
font-weight: 500;
font-size: $mobile-title;
diff --git a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
index 24e864ed41..893390dbc0 100644
--- a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
+++ b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
@@ -78,6 +78,20 @@ const MentionDropdown: FC = (props) => {
return discussions.slice(0, 5);
}, [isShowMoreDiscussions, discussions]);
+ useEffect(() => {
+ const allRefs = document.querySelectorAll(
+ `.${styles.content}`,
+ );
+
+ listRefs.current = [];
+ allRefs.forEach((ref) => listRefs.current.push(ref));
+ listRefs.current.sort((a, b) => {
+ const tabIndexA = parseInt(a.getAttribute("tabIndex") || "0", 10);
+ const tabIndexB = parseInt(b.getAttribute("tabIndex") || "0", 10);
+ return tabIndexA - tabIndexB;
+ });
+ }, [searchText]);
+
useEffect(() => {
if (shouldFocusTarget) {
// Clear and rebuild listRefs based on current usersList and discussionsList
@@ -86,7 +100,6 @@ const MentionDropdown: FC = (props) => {
`.${styles.content}`,
);
allRefs.forEach((ref) => listRefs.current.push(ref));
-
// Sort the listRefs by tabIndex
listRefs.current.sort((a, b) => {
const tabIndexA = parseInt(a.getAttribute("tabIndex") || "0", 10);
@@ -225,8 +238,6 @@ const MentionDropdown: FC = (props) => {
}),
);
- console.log('---discussionId',discussionId, '--optimisticFeedItem',optimisticFeedItem.id);
-
dispatch(
commonActions.createDiscussion.request({
payload: {
@@ -252,6 +263,30 @@ const MentionDropdown: FC = (props) => {
discussions.length === 0;
const isLoading = initUsers.length === 0 && initDiscussions.length === 0;
+ const calculateNewDiscussionTabIndex = () => {
+ let tabIndex = 0;
+
+ // Users
+ if (users.length > 0) {
+ tabIndex += isShowMoreUsers ? users.length : Math.min(users.length, 5); // Visible users
+ if (users.length > 5 && !isShowMoreUsers) {
+ tabIndex += 1; // "Show More Users" button
+ }
+ }
+
+ // Discussions
+ if (discussions.length > 0) {
+ tabIndex += isShowMoreDiscussions
+ ? discussions.length
+ : Math.min(discussions.length, 5); // Visible discussions
+ if (discussions.length > 5 && !isShowMoreDiscussions) {
+ tabIndex += 1; // "Show More Discussions" button
+ }
+ }
+
+ return tabIndex; // The "New Discussion" button follows all visible items
+ };
+
return (
= (props) => {
onKeyDown={onKeyDown}
>
{isLoading && (
- -
+
-
)}
{isEmptyContent && (
- -
+
-
No results
)}
@@ -343,7 +378,7 @@ const MentionDropdown: FC = (props) => {
{(searchText && canCreateDiscussion) && (
- 0 ? usersList.length + 1 : 0) + (discussionsList.length > 0 ? discussionsList.length + 1 : 0)}
+ tabIndex={calculateNewDiscussionTabIndex()}
key="newDiscussion"
data-type="newDiscussion"
onClick={createDiscussion}
From 77f5551051e23ef1594aae172c6cdf55e5764628 Mon Sep 17 00:00:00 2001
From: Pavel Meyer
Date: Wed, 11 Dec 2024 11:06:46 +0300
Subject: [PATCH 07/12] CW-mention-streams
Added creation of discussion
---
.../ChatComponent/ChatComponent.tsx | 1 +
.../components/ChatInput/ChatInput.tsx | 5 ++-
.../DiscussionLink/DiscussionLink.tsx | 36 +++++++++++++++++++
.../components/DiscussionLink/index.ts | 1 +
.../Chat/ChatMessage/components/index.ts | 1 +
.../utils/getTextFromTextEditorString.tsx | 11 +++++-
.../ui-kit/TextEditor/BaseTextEditor.tsx | 9 +++--
.../TextEditor/components/Element/Element.tsx | 21 ++++++++++-
.../MentionDropdown/MentionDropdown.tsx | 9 ++---
.../TextEditor/constants/elementType.ts | 2 ++
.../ui-kit/TextEditor/hofs/withMentions.ts | 4 +--
src/shared/ui-kit/TextEditor/types.ts | 11 +++++-
.../utils/checkIsTextEditorValueEmpty.ts | 2 +-
.../utils/countTextEditorEmojiElements.ts | 2 +-
src/shared/ui-kit/TextEditor/utils/index.ts | 1 +
.../TextEditor/utils/insertDiscussionLink.ts | 18 ++++++++++
.../TextEditor/utils/isRtlWithNoMentions.ts | 2 +-
.../removeTextEditorEmptyEndLinesValues.ts | 1 +
.../utils/serializeTextEditorValue.ts | 4 +++
src/store/states/common/reducer.ts | 4 +--
20 files changed, 127 insertions(+), 18 deletions(-)
create mode 100644 src/shared/components/Chat/ChatMessage/components/DiscussionLink/DiscussionLink.tsx
create mode 100644 src/shared/components/Chat/ChatMessage/components/DiscussionLink/index.ts
create mode 100644 src/shared/ui-kit/TextEditor/utils/insertDiscussionLink.ts
diff --git a/src/pages/common/components/ChatComponent/ChatComponent.tsx b/src/pages/common/components/ChatComponent/ChatComponent.tsx
index 11ae027f83..d48bd19438 100644
--- a/src/pages/common/components/ChatComponent/ChatComponent.tsx
+++ b/src/pages/common/components/ChatComponent/ChatComponent.tsx
@@ -941,6 +941,7 @@ export default function ChatComponent({
user={user}
commonId={commonId}
circleVisibility={discussion?.circleVisibility}
+ onInternalLinkClick={onInternalLinkClick}
/>
diff --git a/src/pages/common/components/ChatComponent/components/ChatInput/ChatInput.tsx b/src/pages/common/components/ChatComponent/components/ChatInput/ChatInput.tsx
index e0311c13eb..69dca3c56f 100644
--- a/src/pages/common/components/ChatComponent/components/ChatInput/ChatInput.tsx
+++ b/src/pages/common/components/ChatComponent/components/ChatInput/ChatInput.tsx
@@ -18,7 +18,7 @@ import {
} from "@/shared/ui-kit";
import { BaseTextEditorHandles } from "@/shared/ui-kit/TextEditor/BaseTextEditor";
import { EmojiCount } from "@/shared/ui-kit/TextEditor/utils";
-import { emptyFunction } from "@/shared/utils";
+import { emptyFunction, InternalLinkData } from "@/shared/utils";
import styles from "./ChatInput.module.scss";
interface ChatInputProps {
@@ -44,6 +44,7 @@ interface ChatInputProps {
circleVisibility?: string[];
user?: User | null;
commonId?: string;
+ onInternalLinkClick?: (data: InternalLinkData) => void;
}
export const ChatInput = React.memo(
@@ -70,6 +71,7 @@ export const ChatInput = React.memo(
circleVisibility,
user,
commonId,
+ onInternalLinkClick,
} = props;
if (shouldHideChatInput) {
@@ -131,6 +133,7 @@ export const ChatInput = React.memo(
circleVisibility={circleVisibility}
user={user}
commonId={commonId}
+ onInternalLinkClick={onInternalLinkClick}
/>