diff --git a/src/shared/components/Dropdown/index.scss b/src/shared/components/Dropdown/index.scss
index 145b3bd27b..36dbfd8af0 100644
--- a/src/shared/components/Dropdown/index.scss
+++ b/src/shared/components/Dropdown/index.scss
@@ -89,7 +89,7 @@
}
.custom-dropdown-wrapper__menu-list {
- max-height: 16.875rem;
+ max-height: 18rem;
margin: 0;
padding: 0;
overflow-y: auto;
diff --git a/src/shared/components/ElementDropdown/ElementDropdown.tsx b/src/shared/components/ElementDropdown/ElementDropdown.tsx
index 1e5c09ef83..a98a795682 100644
--- a/src/shared/components/ElementDropdown/ElementDropdown.tsx
+++ b/src/shared/components/ElementDropdown/ElementDropdown.tsx
@@ -4,7 +4,7 @@ import classNames from "classnames";
import copyToClipboard from "copy-to-clipboard";
import { selectUser } from "@/pages/Auth/store/selectors";
import { MenuButton, ShareModal } from "@/shared/components";
-import { Orientation, EntityTypes } from "@/shared/constants";
+import { Orientation, EntityTypes, ShareButtonText } from "@/shared/constants";
import { useNotification, useModal } from "@/shared/hooks";
import {
CopyIcon,
@@ -165,9 +165,10 @@ const ElementDropdown: FC
= ({
}
if (!isChatMessage) {
+ const shareText = isDiscussionMessage ? ShareButtonText.Message : "Share";
items.push({
- text: } />,
- searchText: "Share",
+ text: } />,
+ searchText: shareText,
value: ElementDropdownMenuItems.Share,
});
}
@@ -261,7 +262,12 @@ const ElementDropdown: FC = ({
switch (selectedItem) {
case ElementDropdownMenuItems.Share:
- onOpen();
+ if(isDiscussionMessage) {
+ copyToClipboard(staticShareLink);
+ notify("The link has been copied!");
+ } else {
+ onOpen();
+ }
break;
case ElementDropdownMenuItems.Copy:
copyToClipboard(
@@ -273,7 +279,7 @@ const ElementDropdown: FC = ({
break;
case ElementDropdownMenuItems.CopyLink:
copyToClipboard(staticShareLink || "");
- notify("The link has copied!");
+ notify("The link has been copied!");
break;
case ElementDropdownMenuItems.Delete:
onOpenDelete();
@@ -312,7 +318,7 @@ const ElementDropdown: FC = ({
const menuInlineStyle = useMemo(
() => ({
- height: `${2.5 * (ElementDropdownMenuItemsList.length || 1)}rem`,
+ height: `${3 * (ElementDropdownMenuItemsList.length || 1)}rem`,
}),
[ElementDropdownMenuItemsList],
);
diff --git a/src/shared/components/ElementDropdown/index.scss b/src/shared/components/ElementDropdown/index.scss
index 8eaeca3cc8..0980cdef69 100644
--- a/src/shared/components/ElementDropdown/index.scss
+++ b/src/shared/components/ElementDropdown/index.scss
@@ -2,8 +2,8 @@
.element-dropdown__menu-wrapper {
.element-dropdown__menu {
- width: 10rem;
- height: 5.625rem;
+ width: 14rem;
+ height: 9rem;
right: 0;
display: flex;
flex-direction: column;
@@ -16,9 +16,9 @@
display: flex;
align-items: center;
box-sizing: border-box;
- height: 2.5rem;
+ height: 3rem;
padding: 0.65625rem 1.5rem;
- font-size: $small;
+ font-size: $xsmall;
box-shadow: inset 0 -0.0625rem 0 var(--drop-shadow);
> :first-child {
diff --git a/src/shared/components/Form/Formik/TextField/TextField.tsx b/src/shared/components/Form/Formik/TextField/TextField.tsx
index c176f14fee..4763f9e6a3 100644
--- a/src/shared/components/Form/Formik/TextField/TextField.tsx
+++ b/src/shared/components/Form/Formik/TextField/TextField.tsx
@@ -1,14 +1,14 @@
-import React, { FC } from "react";
+import React, { forwardRef } from "react";
import { useField } from "formik";
import { useZoomDisabling } from "@/shared/hooks";
-import { Input, InputProps } from "../../Input";
+import { Input, InputProps, InputRef } from "../../Input";
export type TextFieldProps = InputProps & {
isRequired?: boolean;
value?: string;
};
-const TextField: FC = (props) => {
+const TextField = forwardRef((props, ref) => {
const { isRequired, ...restProps } = props;
const [field, { touched, error }] = useField(restProps);
const hintToShow = restProps.hint || (isRequired ? "Required" : "");
@@ -16,12 +16,13 @@ const TextField: FC = (props) => {
return (
);
-};
+});
export default TextField;
diff --git a/src/shared/components/Form/Input/Input.tsx b/src/shared/components/Form/Input/Input.tsx
index 9b1651be04..4c299b78ba 100644
--- a/src/shared/components/Form/Input/Input.tsx
+++ b/src/shared/components/Form/Input/Input.tsx
@@ -79,6 +79,8 @@ const Input: ForwardRefRenderFunction = (
...restProps
} = props;
const innerInputRef = useRef(null);
+ const innerRef = useRef(null);
+
const [inputLengthRef, setInputLengthRef] = useState(
null,
);
@@ -133,7 +135,8 @@ const Input: ForwardRefRenderFunction = (
inputRef,
() => ({
focus: () => {
- innerInputRef.current?.focus();
+ innerInputRef?.current?.focus();
+ innerRef?.current?.focus();
},
}),
[],
@@ -189,10 +192,11 @@ const Input: ForwardRefRenderFunction = (
)}
{restProps.isTextarea && (
+ />
)}
{shouldDisplayCountToUse && !countAsHint && (
diff --git a/src/shared/components/ShareModal/ShareModal.tsx b/src/shared/components/ShareModal/ShareModal.tsx
index 2811daf6cc..f74310a107 100644
--- a/src/shared/components/ShareModal/ShareModal.tsx
+++ b/src/shared/components/ShareModal/ShareModal.tsx
@@ -37,7 +37,7 @@ const ShareModal: FC> = (props) => {
const handleCopyClick = () => {
copyToClipboard(sourceUrl);
- notify("The link has copied!");
+ notify("The link has been copied!");
};
return (
diff --git a/src/shared/constants/index.tsx b/src/shared/constants/index.tsx
index 56d585e63f..1a3a2db9b1 100755
--- a/src/shared/constants/index.tsx
+++ b/src/shared/constants/index.tsx
@@ -36,3 +36,4 @@ export * from "./docChange";
export * from "./files";
export * from "./inboxAction";
export * from "./featureFlags";
+export * from "./shareText";
diff --git a/src/shared/constants/shareText.ts b/src/shared/constants/shareText.ts
new file mode 100644
index 0000000000..a8f89626c0
--- /dev/null
+++ b/src/shared/constants/shareText.ts
@@ -0,0 +1,5 @@
+export enum ShareButtonText {
+ Space = "Copy space link",
+ Stream = "Copy link",
+ Message = "Copy message link"
+}
\ No newline at end of file
diff --git a/src/shared/hooks/useCases/useCommonFeedItems.ts b/src/shared/hooks/useCases/useCommonFeedItems.ts
index f96da830ab..d527fe5f31 100644
--- a/src/shared/hooks/useCases/useCommonFeedItems.ts
+++ b/src/shared/hooks/useCases/useCommonFeedItems.ts
@@ -6,6 +6,7 @@ import {
FeedItems,
selectFeedItems,
selectFilteredFeedItems,
+ selectOptimisticFeedItems,
} from "@/store/states";
interface Return
@@ -21,6 +22,7 @@ export const useCommonFeedItems = (
const dispatch = useDispatch();
const feedItems = useSelector(selectFeedItems);
const filteredFeedItems = useSelector(selectFilteredFeedItems);
+ const optimisticFeedItems = useSelector(selectOptimisticFeedItems);
const idsForNotListeningRef = useRef(idsForNotListening || []);
const isSubscriptionAllowed = feedItems.data !== null;
@@ -53,6 +55,20 @@ export const useCommonFeedItems = (
return;
}
+
+ // TODO: HERE I can get optFeedItem by discussionId
+ const optItemIds = Array.from(optimisticFeedItems.values()).map((item) => {
+ return item.feedItem.data.id;
+ } );
+
+ data.forEach((item) => {
+ const discussionId = item.commonFeedItem.data.discussionId ?? item.commonFeedItem.data.id;
+ if(optItemIds.includes(discussionId)) {
+
+ dispatch(commonActions.removeOptimisticFeedItemState({id: discussionId}))
+ }
+ })
+
const finalData =
idsForNotListeningRef.current.length > 0
? data.filter(
@@ -68,7 +84,7 @@ export const useCommonFeedItems = (
);
return unsubscribe;
- }, [isSubscriptionAllowed, feedItems.firstDocTimestamp, commonId]);
+ }, [isSubscriptionAllowed, feedItems.firstDocTimestamp, commonId, optimisticFeedItems]);
useEffect(() => {
return () => {
diff --git a/src/shared/hooks/useCases/useDiscussionMessagesById.ts b/src/shared/hooks/useCases/useDiscussionMessagesById.ts
index 7e6fc36d32..2dd45e70d8 100644
--- a/src/shared/hooks/useCases/useDiscussionMessagesById.ts
+++ b/src/shared/hooks/useCases/useDiscussionMessagesById.ts
@@ -1,4 +1,4 @@
-import { useState, useCallback } from "react";
+import { useState, useCallback, useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useDeepCompareEffect, useUpdateEffect } from "react-use";
import { trace } from "firebase/performance";
@@ -100,6 +100,8 @@ export const useDiscussionMessagesById = ({
const [discussionMessagesWithOwners, setDiscussionMessagesWithOwners] =
useState();
+ const unsubscribeRef = useRef(null);
+
useUpdateEffect(() => {
if (discussionId) {
setDiscussionMessagesWithOwners([]);
@@ -251,7 +253,7 @@ export const useDiscussionMessagesById = ({
const fetchDiscussionMessagesTrace = trace(perf, 'fetchDiscussionMessages');
fetchDiscussionMessagesTrace.start();
- DiscussionMessageService.subscribeToDiscussionMessagesByDiscussionId(
+ unsubscribeRef.current = DiscussionMessageService.subscribeToDiscussionMessagesByDiscussionId(
discussionId,
lastVisible && lastVisible[discussionId],
async (
@@ -268,16 +270,16 @@ export const useDiscussionMessagesById = ({
...prevVisible,
[discussionId]: lastVisibleDocument,
}));
-
+
const hasLastVisibleDocument = !!lastVisibleDocument?.data();
-
+
const discussionsWithText = await Promise.all(
updatedDiscussionMessages.map(async (discussionMessage) => {
const isUserDiscussionMessage =
checkIsUserDiscussionMessage(discussionMessage);
const isSystemMessage =
checkIsSystemDiscussionMessage(discussionMessage);
-
+
const parsedText = await getTextFromTextEditorString({
userId,
ownerId: isUserDiscussionMessage
@@ -294,7 +296,7 @@ export const useDiscussionMessagesById = ({
onFeedItemClick,
onInternalLinkClick,
});
-
+
return {
...discussionMessage,
parsedText,
@@ -321,10 +323,35 @@ export const useDiscussionMessagesById = ({
},
);
fetchDiscussionMessagesTrace.stop();
- } catch(err) {
+ } catch (err) {
setIsBatchLoading(false);
}
- },[discussionId, isEndOfList, state.loading, state.data, isBatchLoading, lastVisible, userId, users, directParent, getCommonPagePath, getCommonPageAboutTabPath, onUserClick, onFeedItemClick, onInternalLinkClick, dispatch]);
+ }, [
+ discussionId,
+ isEndOfList,
+ state.loading,
+ state.data,
+ isBatchLoading,
+ lastVisible,
+ userId,
+ users,
+ directParent,
+ getCommonPagePath,
+ getCommonPageAboutTabPath,
+ onUserClick,
+ onFeedItemClick,
+ onInternalLinkClick,
+ dispatch,
+ ]);
+
+ useEffect(() => {
+ // Cleanup subscription on unmount or when discussionId changes
+ return () => {
+ if (unsubscribeRef.current) {
+ unsubscribeRef.current();
+ }
+ };
+ }, [discussionId]);
useDeepCompareEffect(() => {
(async () => {
diff --git a/src/shared/hooks/useCases/useUpdateFeedItemSeenState.ts b/src/shared/hooks/useCases/useUpdateFeedItemSeenState.ts
index d63b56d3ab..09cd016218 100644
--- a/src/shared/hooks/useCases/useUpdateFeedItemSeenState.ts
+++ b/src/shared/hooks/useCases/useUpdateFeedItemSeenState.ts
@@ -1,8 +1,9 @@
-import { useCallback } from "react";
+import { useCallback, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { selectUser } from "@/pages/Auth/store/selectors";
import { CommonFeedService } from "@/services";
import { getFeedItemUserMetadataKey } from "@/shared/constants";
+import axios, { CancelTokenSource } from "axios";
import {
MarkCommonFeedItemAsSeenPayload,
MarkCommonFeedItemAsUnseenPayload,
@@ -21,6 +22,9 @@ export const useUpdateFeedItemSeenState = (): Return => {
const dispatch = useDispatch();
const user = useSelector(selectUser());
const userId = user?.uid;
+
+ // Ref to store the current CancelTokenSource
+ const cancelTokenRef = useRef(null);
const updateSeenState = async (
payload:
@@ -32,6 +36,15 @@ export const useUpdateFeedItemSeenState = (): Return => {
return;
}
+ // Cancel the previous request if it exists
+ if (cancelTokenRef.current) {
+ cancelTokenRef.current.cancel("Operation canceled due to a new request.");
+ }
+
+ // Create a new CancelToken for the current request
+ const cancelTokenSource = axios.CancelToken.source();
+ cancelTokenRef.current = cancelTokenSource;
+
const { commonId, feedObjectId } = payload;
const key = getFeedItemUserMetadataKey({
commonId,
@@ -49,11 +62,27 @@ export const useUpdateFeedItemSeenState = (): Return => {
);
if (newSeenValue) {
- await CommonFeedService.markCommonFeedItemAsSeen(payload);
+ await CommonFeedService.markCommonFeedItemAsSeen(payload, {
+ cancelToken: cancelTokenSource.token,
+ });
} else {
- await CommonFeedService.markCommonFeedItemAsUnseen(payload);
+ await CommonFeedService.markCommonFeedItemAsUnseen(payload, {
+ cancelToken: cancelTokenSource.token,
+ });
}
- } catch (error) {
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ if (error.name === 'AbortError') {
+ console.log('Request was aborted');
+ return;
+ }
+
+ // Handle other types of errors here, like logging or displaying a message
+ console.error("An error occurred:", error.message);
+ } else {
+ console.error("An unknown error occurred");
+ }
+
dispatch(
cacheActions.updateFeedItemUserSeenState({
key,
diff --git a/src/shared/models/CommonFeed.tsx b/src/shared/models/CommonFeed.tsx
index 3d264dc525..ef8cccabce 100644
--- a/src/shared/models/CommonFeed.tsx
+++ b/src/shared/models/CommonFeed.tsx
@@ -1,10 +1,13 @@
import { DiscussionMessageOwnerType } from "@/shared/constants";
import { BaseEntity } from "./BaseEntity";
+import { Discussion } from "./Discussion";
import { SoftDeleteEntity } from "./SoftDeleteEntity";
export enum CommonFeedType {
Proposal = "Proposal",
Discussion = "Discussion",
+ OptimisticDiscussion = "OptimisticDiscussion",
+ OptimisticProposal = "OptimisticProposal",
Project = "Project",
PayIn = "PayIn",
ProjectCreation = "ProjectCreation",
@@ -12,6 +15,24 @@ export enum CommonFeedType {
JoinProjectInCommon = "JoinProjectInCommon",
}
+export enum OptimisticFeedItemState {
+ loading = 'loading',
+ rejected = 'failed',
+ fulfilled = 'fulfilled'
+}
+
+export interface LastMessageContent {
+ userName: string;
+ ownerId: string;
+ content: string;
+ ownerType?: DiscussionMessageOwnerType;
+}
+
+export type DiscussionWithOptimisticData = Discussion & {
+ state?: OptimisticFeedItemState; // Optional state property
+ lastMessageContent: LastMessageContent; // Additional property
+};
+
export interface CommonFeed extends BaseEntity, SoftDeleteEntity {
userId: string;
commonId: string;
@@ -19,14 +40,10 @@ export interface CommonFeed extends BaseEntity, SoftDeleteEntity {
type: CommonFeedType;
id: string;
discussionId: string | null;
- lastMessage?: {
- userName: string;
- ownerId: string;
- content: string;
- ownerType?: DiscussionMessageOwnerType;
- };
+ lastMessage?: LastMessageContent;
hasFiles?: boolean;
hasImages?: boolean;
};
+ optimisticData?: DiscussionWithOptimisticData;
circleVisibility: string[];
}
diff --git a/src/shared/models/governance/proposals/BasicArgsProposal.ts b/src/shared/models/governance/proposals/BasicArgsProposal.ts
index 76f615e772..4078d30ddb 100644
--- a/src/shared/models/governance/proposals/BasicArgsProposal.ts
+++ b/src/shared/models/governance/proposals/BasicArgsProposal.ts
@@ -1,6 +1,10 @@
import { CommonLink } from "../../Common";
export interface BasicArgsProposal {
+ id: string;
+
+ discussionId: string;
+
readonly commonId: string;
readonly proposerId: string;
diff --git a/src/shared/ui-kit/Loader/Loader.module.scss b/src/shared/ui-kit/Loader/Loader.module.scss
index 2129d49978..8153df74fe 100644
--- a/src/shared/ui-kit/Loader/Loader.module.scss
+++ b/src/shared/ui-kit/Loader/Loader.module.scss
@@ -6,6 +6,11 @@
height: 3.125rem;
}
+.bigLoader {
+ height: 4.6875rem;
+ width: 4.6875rem;
+}
+
.globalLoaderOverlay {
position: fixed;
top: 0;
diff --git a/src/shared/ui-kit/Loader/Loader.tsx b/src/shared/ui-kit/Loader/Loader.tsx
index 4575d3db6e..f7b3bd4f5d 100644
--- a/src/shared/ui-kit/Loader/Loader.tsx
+++ b/src/shared/ui-kit/Loader/Loader.tsx
@@ -8,6 +8,7 @@ import styles from "./Loader.module.scss";
export enum LoaderVariant {
Default,
Global,
+ Big,
}
export enum LoaderColor {
@@ -35,7 +36,9 @@ const Loader: FC = (props) => {
const [isShowing, setIsShowing] = useState(!delay);
const loaderEl = (
diff --git a/src/shared/ui-kit/SuspenseLoader/SuspenseLoader.module.scss b/src/shared/ui-kit/SuspenseLoader/SuspenseLoader.module.scss
new file mode 100644
index 0000000000..52522947d8
--- /dev/null
+++ b/src/shared/ui-kit/SuspenseLoader/SuspenseLoader.module.scss
@@ -0,0 +1,16 @@
+.container {
+ width: 100vw;
+ height: 100vh;
+ display: flex;
+ background-color: white;
+ flex: 4;
+ justify-content: center;
+ align-items: center;
+}
+
+.loader {
+ display: flex;
+ flex-direction:column;
+ justify-content: center;
+ align-items: center;
+}
\ No newline at end of file
diff --git a/src/shared/ui-kit/SuspenseLoader/SuspenseLoader.tsx b/src/shared/ui-kit/SuspenseLoader/SuspenseLoader.tsx
new file mode 100644
index 0000000000..426d822941
--- /dev/null
+++ b/src/shared/ui-kit/SuspenseLoader/SuspenseLoader.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Loader, LoaderVariant } from "../Loader";
+import styles from "./SuspenseLoader.module.scss";
+
+
+export default (
+
+);
\ No newline at end of file
diff --git a/src/shared/ui-kit/SuspenseLoader/index.ts b/src/shared/ui-kit/SuspenseLoader/index.ts
new file mode 100644
index 0000000000..fa71f15443
--- /dev/null
+++ b/src/shared/ui-kit/SuspenseLoader/index.ts
@@ -0,0 +1 @@
+export { default as SuspenseLoader } from "./SuspenseLoader";
\ No newline at end of file
diff --git a/src/shared/ui-kit/TextEditor/BaseTextEditor.module.scss b/src/shared/ui-kit/TextEditor/BaseTextEditor.module.scss
index 7ce5c294c1..8b3d6c9539 100644
--- a/src/shared/ui-kit/TextEditor/BaseTextEditor.module.scss
+++ b/src/shared/ui-kit/TextEditor/BaseTextEditor.module.scss
@@ -2,4 +2,5 @@
position: relative;
width: 100%;
height: 100%;
+ scroll-behavior: smooth;
}
diff --git a/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx b/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx
index 929cac0ceb..87e1841587 100644
--- a/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx
+++ b/src/shared/ui-kit/TextEditor/BaseTextEditor.tsx
@@ -1,5 +1,4 @@
import React, {
- FC,
FocusEventHandler,
KeyboardEvent,
MutableRefObject,
@@ -8,6 +7,8 @@ import React, {
useMemo,
useState,
useCallback,
+ useImperativeHandle,
+ forwardRef,
} from "react";
import { useDebounce } from "react-use";
import classNames from "classnames";
@@ -44,6 +45,13 @@ 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;
+ clear: () => void;
+}
export interface TextEditorProps {
className?: string;
@@ -51,8 +59,8 @@ export interface TextEditorProps {
emojiContainerClassName?: string;
emojiPickerContainerClassName?: string;
inputContainerRef?:
- | MutableRefObject
- | RefCallback;
+ | MutableRefObject
+ | RefCallback;
editorRef?: MutableRefObject | RefCallback;
id?: string;
name?: string;
@@ -81,7 +89,7 @@ const INITIAL_SEARCH_VALUE = {
},
};
-const BaseTextEditor: FC = (props) => {
+const BaseTextEditor = forwardRef((props, ref) => {
const {
className,
classNameRtl,
@@ -117,6 +125,13 @@ const BaseTextEditor: FC = (props) => {
),
[],
);
+ 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);
@@ -131,11 +146,7 @@ const BaseTextEditor: FC = (props) => {
[value],
);
- useEffect(() => {
- if (!shouldReinitializeEditor) {
- return;
- }
-
+ const clearInput = () => {
setTimeout(() => {
Transforms.delete(editor, {
at: {
@@ -154,8 +165,37 @@ const BaseTextEditor: FC = (props) => {
const editorEl = ReactEditor.toDOMNode(editor, editor);
editorEl.scrollTo(0, 0);
onClearFinished();
- }, 0);
- }, [shouldReinitializeEditor, 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
+ const editorEl = ReactEditor.toDOMNode(editor, editor);
+ editorEl.focus();
+
+ // Ensure the editor itself is focused programmatically
+ ReactEditor.focus(editor);
+ }
+ },
+ clear: () => {
+ clearInput();
+ }
+ }));
useEffect(() => {
if (!editorRef) {
@@ -205,7 +245,7 @@ const BaseTextEditor: FC = (props) => {
}
};
- const chars = (users ?? []).filter((user) => {
+ const chars = (usersWithAI ?? []).filter((user) => {
return getUserName(user)
?.toLowerCase()
.startsWith(search.text.substring(1).toLowerCase());
@@ -234,7 +274,8 @@ const BaseTextEditor: FC = (props) => {
event.preventDefault();
setShouldFocusTarget(true);
} else {
- onKeyDown && onKeyDown(event);
+ // event.stopPropagation();
+ onKeyDown && onKeyDown(event); // Call any custom onKeyDown handler
if (event.key === KeyboardKeys.Enter && !isMobile()) {
onToggleIsMessageSent();
}
@@ -318,6 +359,16 @@ const BaseTextEditor: FC = (props) => {
[onChange, value, handleMentionSelectionChange],
);
+ const customScrollSelectionIntoView = ( ) => {
+ if (inputContainerRef && 'current' in inputContainerRef && inputContainerRef?.current) {
+ inputContainerRef.current?.scrollIntoView({
+ behavior: "smooth",
+ block: "end",
+ inline: "nearest",
+ });
+ }
+ }
+
return (
@@ -336,7 +387,7 @@ const BaseTextEditor: FC = (props) => {
disabled={disabled}
onBlur={onBlur}
onKeyDown={handleKeyDown}
- scrollSelectionIntoView={scrollSelectionIntoView}
+ scrollSelectionIntoView={scrollSelectionIntoView ?? customScrollSelectionIntoView}
elementStyles={elementStyles}
/>
= (props) => {
);
-};
+});
export default BaseTextEditor;
diff --git a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
index e4a2f3c24d..583477cc13 100644
--- a/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
+++ b/src/shared/ui-kit/TextEditor/components/MentionDropdown/MentionDropdown.tsx
@@ -1,10 +1,8 @@
import React, { FC, useEffect, useMemo, useRef, useState } from "react";
import { uniq } from "lodash";
import { UserAvatar } from "@/shared/components";
-import { AI_PRO_USER, AI_USER, FeatureFlags } from "@/shared/constants";
import { KeyboardKeys } from "@/shared/constants/keyboardKeys";
import { useOutsideClick } from "@/shared/hooks";
-import { useFeatureFlag } from "@/shared/hooks/useFeatureFlag";
import { User } from "@/shared/models";
import { Loader } from "@/shared/ui-kit";
import { getUserName } from "@/shared/utils";
@@ -22,20 +20,15 @@ export interface MentionDropdownProps {
const MentionDropdown: FC = (props) => {
const {
onClick,
- users: initialUsers = [],
+ users = [],
onClose,
shouldFocusTarget,
} = props;
const mentionRef = useRef(null);
const listRefs = useRef([]);
- const featureFlags = useFeatureFlag();
- const isAiBotProEnabled = featureFlags?.get(FeatureFlags.AiBotPro);
const { isOutside, setOutsideValue } = useOutsideClick(mentionRef);
const [index, setIndex] = useState(0);
- const users = useMemo(
- () => [isAiBotProEnabled ? AI_PRO_USER : AI_USER, ...initialUsers],
- [initialUsers, isAiBotProEnabled],
- );
+
const userIds = useMemo(() => users.map(({ uid }) => uid), [users]);
useEffect(() => {
diff --git a/src/shared/ui-kit/index.ts b/src/shared/ui-kit/index.ts
index 1e56588102..faf1e88e6b 100644
--- a/src/shared/ui-kit/index.ts
+++ b/src/shared/ui-kit/index.ts
@@ -27,3 +27,4 @@ export * from "./TopNavigation";
export * from "./UploadFiles";
export * from "./FilePreview";
export * from "./MenuButton";
+export * from "./SuspenseLoader";
diff --git a/src/shared/utils/generateFirstMessage.ts b/src/shared/utils/generateFirstMessage.ts
new file mode 100644
index 0000000000..836f289d26
--- /dev/null
+++ b/src/shared/utils/generateFirstMessage.ts
@@ -0,0 +1,3 @@
+export const generateFirstMessage = ({ userName, userId }: { userName: string; userId: string }): string => {
+ return `[{"type":"paragraph","children":[{"text":"This discussion was created by "},{"type":"mention","displayName":"${userName} ","userId":"${userId}","children":[{"text":""}]},{"text":""}]}]`
+}
\ No newline at end of file
diff --git a/src/shared/utils/generateOptimisticFeedItem.ts b/src/shared/utils/generateOptimisticFeedItem.ts
new file mode 100644
index 0000000000..df9731e3d9
--- /dev/null
+++ b/src/shared/utils/generateOptimisticFeedItem.ts
@@ -0,0 +1,66 @@
+import { Timestamp as FirestoreTimestamp } from "firebase/firestore";
+import { v4 as uuidv4 } from "uuid";
+import { CommonFeed, CommonFeedType, LastMessageContent, OptimisticFeedItemState } from "../models";
+
+interface GenerateOptimisticFeedItemPayload {
+ userId: string;
+ discussionId: string;
+ commonId: string;
+ type: CommonFeedType,
+ title: string;
+ content: string;
+ circleVisibility: string[];
+ lastMessageContent: LastMessageContent
+}
+
+export const generateOptimisticFeedItem = ({
+ userId,
+ discussionId,
+ commonId,
+ type,
+ title,
+ content,
+ circleVisibility,
+ lastMessageContent,
+}: GenerateOptimisticFeedItemPayload): CommonFeed => {
+
+ const optimisticFeedItemId = uuidv4();
+ const currentDate = FirestoreTimestamp.now();
+ return {
+ id: optimisticFeedItemId,
+ createdAt: currentDate,
+ updatedAt: currentDate,
+ isDeleted: false,
+ userId,
+ commonId,
+ data: {
+ type,
+ id: discussionId,
+ discussionId: null,
+ hasFiles: false,
+ hasImages: false,
+ },
+ optimisticData: {
+ id: discussionId,
+ title: title,
+ message: content,
+ ownerId: userId,
+ commonId,
+ lastMessage: currentDate,
+ lastMessageContent,
+ updatedAt: currentDate,
+ createdAt: currentDate,
+ messageCount: 0,
+ followers: [],
+ files: [],
+ images: [],
+ discussionMessages: [],
+ isDeleted: false,
+ circleVisibility,
+ circleVisibilityByCommon: null,
+ linkedCommonIds: [],
+ state: OptimisticFeedItemState.loading,
+ },
+ circleVisibility,
+ }
+}
\ No newline at end of file
diff --git a/src/shared/utils/index.tsx b/src/shared/utils/index.tsx
index af54f66cf5..085e0370a0 100755
--- a/src/shared/utils/index.tsx
+++ b/src/shared/utils/index.tsx
@@ -45,3 +45,6 @@ export * from "./joinWithLast";
export * from "./getResizedFileUrl";
export * from "./areTimestampsEqual";
export * from "./parseMessageLink";
+export * from "./sortByTierDesc";
+export * from "./generateOptimisticFeedItem";
+export * from "./generateFirstMessage";
diff --git a/src/shared/utils/sortByTierDesc.ts b/src/shared/utils/sortByTierDesc.ts
new file mode 100644
index 0000000000..c05e6af793
--- /dev/null
+++ b/src/shared/utils/sortByTierDesc.ts
@@ -0,0 +1,8 @@
+type TierSortable = {
+ tier?: number | null;
+ };
+
+export function sortByTierDesc(array: T[]): T[] {
+ return [...array].sort((a, b) => (b.tier ?? 0) - (a.tier ?? 0));
+ }
+
\ No newline at end of file
diff --git a/src/store/states/common/actions.ts b/src/store/states/common/actions.ts
index d2a203959c..df2c86b1d7 100644
--- a/src/store/states/common/actions.ts
+++ b/src/store/states/common/actions.ts
@@ -17,6 +17,7 @@ import {
CommonMember,
Discussion,
Governance,
+ OptimisticFeedItemState,
Proposal,
} from "@/shared/models";
import { CommonActionType } from "./constants";
@@ -27,6 +28,7 @@ import {
FeedItemsPayload,
PinnedFeedItems,
} from "./types";
+import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages";
export const resetCommon = createStandardAction(
CommonActionType.RESET_COMMON,
@@ -218,6 +220,35 @@ export const setSharedFeedItem = createStandardAction(
CommonActionType.SET_SHARED_FEED_ITEM,
)();
+export const setOptimisticFeedItem = createStandardAction(
+ CommonActionType.SET_OPTIMISTIC_FEED_ITEM,
+)();
+
+export const updateOptimisticFeedItemState = createStandardAction(
+ CommonActionType.UPDATE_OPTIMISTIC_FEED_ITEM,
+)<{
+ id: string;
+ state: OptimisticFeedItemState;
+}>();
+
+export const removeOptimisticFeedItemState = createStandardAction(
+ CommonActionType.REMOVE_OPTIMISTIC_FEED_ITEM,
+)<{
+ id: string;
+}>();
+
+export const setOptimisticDiscussionMessages = createStandardAction(
+ CommonActionType.SET_OPTIMISTIC_DISCUSSION_MESSAGES,
+)();
+
+export const clearOptimisticDiscussionMessages = createStandardAction(
+ CommonActionType.CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES,
+)();
+
+export const clearCreatedOptimisticFeedItem = createStandardAction(
+ CommonActionType.CLEAR_CREATED_OPTIMISTIC_FEED_ITEM,
+)();
+
export const setRecentStreamId = createStandardAction(
CommonActionType.SET_RECENT_STREAM_ID,
)();
diff --git a/src/store/states/common/constants.ts b/src/store/states/common/constants.ts
index 9a4c118b8f..4423ef7835 100644
--- a/src/store/states/common/constants.ts
+++ b/src/store/states/common/constants.ts
@@ -56,6 +56,15 @@ export enum CommonActionType {
SET_SHARED_FEED_ITEM_ID = "@COMMON/SET_SHARED_FEED_ITEM_ID",
SET_SHARED_FEED_ITEM = "@COMMON/SET_SHARED_FEED_ITEM",
+ SET_OPTIMISTIC_FEED_ITEM = "@COMMON/SET_OPTIMISTIC_FEED_ITEM",
+ UPDATE_OPTIMISTIC_FEED_ITEM = "@COMMON/UPDATE_OPTIMISTIC_FEED_ITEM",
+ REMOVE_OPTIMISTIC_FEED_ITEM = "@COMMON/REMOVE_OPTIMISTIC_FEED_ITEM",
+
+ SET_OPTIMISTIC_DISCUSSION_MESSAGES = "@COMMON/SET_OPTIMISTIC_DISCUSSION_MESSAGES",
+ CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES = "@COMMON/CLEAR_OPTIMISTIC_DISCUSSION_MESSAGES",
+
+ CLEAR_CREATED_OPTIMISTIC_FEED_ITEM = "@COMMON/CLEAR_CREATED_OPTIMISTIC_FEED_ITEM",
+
SET_RECENT_STREAM_ID = "@COMMON/SET_RECENT_STREAM_ID",
SET_RECENT_ASSIGNED_CIRCLE_BY_MEMBER = "@COMMON/SET_RECENT_ASSIGNED_CIRCLE_BY_MEMBER",
diff --git a/src/store/states/common/reducer.ts b/src/store/states/common/reducer.ts
index 90e5f0d544..a5b23ff3fd 100644
--- a/src/store/states/common/reducer.ts
+++ b/src/store/states/common/reducer.ts
@@ -50,6 +50,9 @@ const initialState: CommonState = {
searchState: { ...initialSearchState },
sharedFeedItemId: null,
sharedFeedItem: null,
+ optimisticFeedItems: new Map(),
+ optimisticDiscussionMessages: new Map(),
+ createdOptimisticFeedItems: new Map(),
commonAction: null,
discussionCreation: {
data: null,
@@ -356,13 +359,12 @@ export const reducer = createReducer(initialState)
};
}),
)
- .handleAction(actions.createDiscussion.success, (state, { payload }) =>
+ .handleAction(actions.createDiscussion.success, (state) =>
produce(state, (nextState) => {
nextState.discussionCreation = {
loading: false,
data: null,
};
- nextState.recentStreamId = payload.id;
}),
)
.handleAction(actions.createDiscussion.failure, (state) =>
@@ -415,13 +417,12 @@ export const reducer = createReducer(initialState)
actions.createSurveyProposal.success,
actions.createFundingProposal.success,
],
- (state, { payload }) =>
+ (state) =>
produce(state, (nextState) => {
nextState.proposalCreation = {
loading: false,
data: null,
};
- nextState.recentStreamId = payload.id;
}),
)
.handleAction(
@@ -449,9 +450,9 @@ export const reducer = createReducer(initialState)
produce(state, (nextState) => {
const payloadData = nextState.sharedFeedItemId
? payload.data &&
- payload.data.filter(
- (item) => item.feedItem.id !== nextState.sharedFeedItemId,
- )
+ payload.data.filter(
+ (item) => item.feedItem.id !== nextState.sharedFeedItemId,
+ )
: payload.data;
const nextData = nextState.feedItems.data || [];
const filteredPayloadData =
@@ -688,10 +689,97 @@ export const reducer = createReducer(initialState)
produce(state, (nextState) => {
nextState.sharedFeedItem = payload
? {
- type: InboxItemType.FeedItemFollow,
- itemId: payload.id,
- feedItem: payload,
- }
+ type: InboxItemType.FeedItemFollow,
+ itemId: payload.id,
+ feedItem: payload,
+ }
: null;
}),
+ )
+ .handleAction(actions.setOptimisticFeedItem, (state, { payload }) =>
+ produce(state, (nextState) => {
+ const updatedMap = new Map(nextState.optimisticFeedItems);
+
+ const optimisticItemId = payload.data.discussionId ?? payload.data.id;
+ // Add the new item to the Map
+ updatedMap.set(optimisticItemId, {
+ type: InboxItemType.FeedItemFollow,
+ itemId: payload.id,
+ feedItem: payload,
+ });
+
+ // Assign the new Map back to the state
+ nextState.optimisticFeedItems = updatedMap;
+ nextState.recentStreamId = optimisticItemId;
+ }),
+ )
+ .handleAction(actions.updateOptimisticFeedItemState, (state, { payload }) =>
+ produce(state, (nextState) => {
+ const updatedMap = new Map(nextState.optimisticFeedItems);
+
+ const optimisticFeedItem = updatedMap.get(payload.id);
+ // Add the new item to the Map
+
+ if(optimisticFeedItem && optimisticFeedItem?.feedItem.optimisticData) {
+ updatedMap.set(payload.id, {
+ ...optimisticFeedItem,
+ feedItem: {
+ ...optimisticFeedItem?.feedItem,
+ optimisticData: {
+ ...optimisticFeedItem.feedItem.optimisticData,
+ state: payload.state
+ }
+ }
+ });
+ }
+
+ // Assign the new Map back to the state
+ nextState.optimisticFeedItems = updatedMap;
+ }),
+ )
+ .handleAction(actions.removeOptimisticFeedItemState, (state, { payload }) =>
+ produce(state, (nextState) => {
+ const createdOptimisticFeedItemsMap = new Map(nextState.createdOptimisticFeedItems);
+ const updatedMap = new Map(nextState.optimisticFeedItems);
+
+ createdOptimisticFeedItemsMap.set(payload.id, updatedMap.get(payload.id));
+ updatedMap.delete(payload.id);
+
+ // Assign the new Map back to the state
+ nextState.optimisticFeedItems = updatedMap;
+ nextState.createdOptimisticFeedItems = createdOptimisticFeedItemsMap;
+ }),
+ )
+ .handleAction(actions.setOptimisticDiscussionMessages, (state, { payload }) =>
+ produce(state, (nextState) => {
+ const updatedMap = new Map(nextState.optimisticDiscussionMessages);
+
+ const discussionMessages = updatedMap.get(payload.discussionId) ?? [];
+ discussionMessages.push(payload);
+ // Add the new item to the Map
+ updatedMap.set(payload.discussionId, discussionMessages);
+
+ // Assign the new Map back to the state
+ nextState.optimisticDiscussionMessages = updatedMap;
+ }),
+ )
+ .handleAction(actions.clearOptimisticDiscussionMessages, (state, { payload }) =>
+ produce(state, (nextState) => {
+ const updatedMap = new Map(nextState.optimisticDiscussionMessages);
+
+ updatedMap.delete(payload);
+
+ // Assign the new Map back to the state
+ nextState.optimisticDiscussionMessages = updatedMap;
+ }),
+ )
+ .handleAction(actions.clearCreatedOptimisticFeedItem, (state, { payload }) =>
+ produce(state, (nextState) => {
+ const updatedMap = new Map(nextState.createdOptimisticFeedItems);
+
+ updatedMap.delete(payload);
+
+ // Assign the new Map back to the state
+ nextState.createdOptimisticFeedItems = updatedMap;
+ }),
);
diff --git a/src/store/states/common/selectors.ts b/src/store/states/common/selectors.ts
index 025cf24e45..541fd87c23 100644
--- a/src/store/states/common/selectors.ts
+++ b/src/store/states/common/selectors.ts
@@ -48,6 +48,15 @@ export const selectSharedFeedItem = (state: AppState) =>
export const selectRecentStreamId = (state: AppState) =>
state.common.recentStreamId;
+export const selectOptimisticFeedItems = (state: AppState) =>
+ state.common.optimisticFeedItems;
+
+export const selectOptimisticDiscussionMessages = (state: AppState) =>
+ state.common.optimisticDiscussionMessages;
+
+export const selectCreatedOptimisticFeedItems = (state: AppState) =>
+ state.common.createdOptimisticFeedItems;
+
export const selectRecentAssignedCircle =
(memberId: string) => (state: AppState) =>
state.common.recentAssignedCircleByMember[memberId];
diff --git a/src/store/states/common/types.ts b/src/store/states/common/types.ts
index 0d9eee745d..d21b4d50b0 100644
--- a/src/store/states/common/types.ts
+++ b/src/store/states/common/types.ts
@@ -4,6 +4,7 @@ import {
NewDiscussionCreationFormValues,
NewProposalCreationFormValues,
} from "@/shared/interfaces";
+import { CreateDiscussionMessageDto } from "@/shared/interfaces/api/discussionMessages";
import { Circle, CommonMember, Governance, Timestamp } from "@/shared/models";
export type EntityCreation = {
@@ -44,6 +45,9 @@ export interface CommonState {
pinnedFeedItems: PinnedFeedItems;
sharedFeedItemId: string | null;
sharedFeedItem: FeedItemFollowLayoutItem | null;
+ createdOptimisticFeedItems: Map;
+ optimisticFeedItems: Map;
+ optimisticDiscussionMessages: Map;
commonAction: CommonAction | null;
discussionCreation: EntityCreation;
proposalCreation: EntityCreation;