diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMembersChip.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMembersChip.tsx new file mode 100644 index 000000000..192b1ed3a --- /dev/null +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMembersChip.tsx @@ -0,0 +1,16 @@ +import { MessageThreadSubscribersDropdownButton } from '@/activities/emails/components/MessageThreadSubscribersDropdownButton'; +import { MessageThread } from '@/activities/emails/types/MessageThread'; + +export const EmailThreadMembersChip = ({ + messageThread, +}: { + messageThread: MessageThread; +}) => { + const subscribers = messageThread.subscribers ?? []; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriber.tsx b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriber.tsx new file mode 100644 index 000000000..9196dd26b --- /dev/null +++ b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriber.tsx @@ -0,0 +1,40 @@ +import { MessageThreadSubscriberDropdownAddSubscriberMenuItem } from '@/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriberMenuItem'; +import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; + +export const MessageThreadSubscriberDropdownAddSubscriber = ({ + existingSubscribers, +}: { + existingSubscribers: MessageThreadSubscriber[]; +}) => { + const { records: workspaceMembersLeftToAdd } = + useFindManyRecords({ + objectNameSingular: CoreObjectNameSingular.WorkspaceMember, + filter: { + not: { + id: { + in: existingSubscribers.map( + ({ workspaceMember }) => workspaceMember.id, + ), + }, + }, + }, + }); + + return ( + + + + {workspaceMembersLeftToAdd.map((workspaceMember) => ( + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriberMenuItem.tsx b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriberMenuItem.tsx new file mode 100644 index 000000000..b56cdc080 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriberMenuItem.tsx @@ -0,0 +1,43 @@ +import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { MenuItemAvatar } from '@/ui/navigation/menu-item/components/MenuItemAvatar'; +import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; +import { IconPlus } from 'twenty-ui'; + +export const MessageThreadSubscriberDropdownAddSubscriberMenuItem = ({ + workspaceMember, +}: { + workspaceMember: WorkspaceMember; +}) => { + const text = `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`; + + const { createOneRecord } = useCreateOneRecord({ + objectNameSingular: CoreObjectNameSingular.MessageThreadSubscriber, + }); + + const handleAddButtonClick = () => { + createOneRecord({ + workspaceMember, + }); + }; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersChip.tsx b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersChip.tsx new file mode 100644 index 000000000..d10827215 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersChip.tsx @@ -0,0 +1,80 @@ +import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useContext } from 'react'; +import { + Avatar, + AvatarGroup, + Chip, + ChipVariant, + IconChevronDown, + ThemeContext, +} from 'twenty-ui'; + +const MAX_NUMBER_OF_AVATARS = 3; + +export const MessageThreadSubscribersChip = ({ + messageThreadSubscribers, +}: { + messageThreadSubscribers: MessageThreadSubscriber[]; +}) => { + const { theme } = useContext(ThemeContext); + + const numberOfMessageThreadSubscribers = messageThreadSubscribers.length; + + const isOnlyOneSubscriber = numberOfMessageThreadSubscribers === 1; + + const isPrivateThread = isOnlyOneSubscriber; + + const privateLabel = 'Private'; + + const susbcriberAvatarUrls = messageThreadSubscribers + .map((member) => member.workspaceMember.avatarUrl) + .filter(isNonEmptyString); + + const firstAvatarUrl = susbcriberAvatarUrls[0]; + const firstAvatarColorSeed = messageThreadSubscribers?.[0].workspaceMember.id; + const firstAvatarPlaceholder = + messageThreadSubscribers?.[0].workspaceMember.name.firstName; + + const subscriberNames = messageThreadSubscribers.map( + (member) => member.workspaceMember?.name.firstName, + ); + + const moreAvatarsLabel = + numberOfMessageThreadSubscribers > MAX_NUMBER_OF_AVATARS + ? `+${numberOfMessageThreadSubscribers - MAX_NUMBER_OF_AVATARS}` + : null; + + const label = isPrivateThread ? privateLabel : moreAvatarsLabel ?? ''; + + return ( + + ) : ( + ( + + ))} + /> + ) + } + rightComponent={} + clickable + /> + ); +}; diff --git a/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersDropdownButton.tsx b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersDropdownButton.tsx new file mode 100644 index 000000000..7f7c42e7a --- /dev/null +++ b/packages/twenty-front/src/modules/activities/emails/components/MessageThreadSubscribersDropdownButton.tsx @@ -0,0 +1,110 @@ +import { offset } from '@floating-ui/react'; +import { IconMinus, IconPlus } from 'twenty-ui'; + +import { MessageThreadSubscriberDropdownAddSubscriber } from '@/activities/emails/components/MessageThreadSubscriberDropdownAddSubscriber'; +import { MessageThreadSubscribersChip } from '@/activities/emails/components/MessageThreadSubscribersChip'; +import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { useListenRightDrawerClose } from '@/ui/layout/right-drawer/hooks/useListenRightDrawerClose'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { MenuItemAvatar } from '@/ui/navigation/menu-item/components/MenuItemAvatar'; +import { useState } from 'react'; + +export const MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID = + 'message-thread-subscriber'; + +export const MessageThreadSubscribersDropdownButton = ({ + messageThreadSubscribers, +}: { + messageThreadSubscribers: MessageThreadSubscriber[]; +}) => { + const [isAddingSubscriber, setIsAddingSubscriber] = useState(false); + + const { closeDropdown } = useDropdown(MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID); + + const mockSubscribers = [ + ...messageThreadSubscribers, + ...messageThreadSubscribers, + ...messageThreadSubscribers, + ...messageThreadSubscribers, + ]; + + // TODO: implement + const handleAddSubscriberClick = () => { + setIsAddingSubscriber(true); + }; + + // TODO: implement + const handleRemoveSubscriber = (_subscriber: MessageThreadSubscriber) => { + closeDropdown(); + }; + + useListenRightDrawerClose(() => { + closeDropdown(); + }); + + return ( + + } + dropdownComponents={ + + {isAddingSubscriber ? ( + + ) : ( + + {messageThreadSubscribers?.map((subscriber) => ( + { + handleRemoveSubscriber(subscriber); + }} + text={ + subscriber.workspaceMember.name.firstName + + ' ' + + subscriber.workspaceMember.name.lastName + } + avatar={{ + placeholder: subscriber.workspaceMember.name.firstName, + avatarUrl: subscriber.workspaceMember.avatarUrl, + placeholderColorSeed: subscriber.workspaceMember.id, + size: 'md', + type: 'rounded', + }} + iconButtons={[ + { + Icon: IconMinus, + onClick: () => { + handleRemoveSubscriber(subscriber); + }, + }, + ]} + /> + ))} + + + + )} + + } + dropdownHotkeyScope={{ + scope: MESSAGE_THREAD_SUBSCRIBER_DROPDOWN_ID, + }} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory.ts b/packages/twenty-front/src/modules/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory.ts index a634721a1..d141d867b 100644 --- a/packages/twenty-front/src/modules/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory.ts +++ b/packages/twenty-front/src/modules/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory.ts @@ -2,7 +2,13 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory'; export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperationSignatureFactory = - ({ messageThreadId }: { messageThreadId: string }) => ({ + ({ + messageThreadId, + isSubscribersEnabled, + }: { + messageThreadId: string; + isSubscribersEnabled: boolean; + }) => ({ objectNameSingular: CoreObjectNameSingular.Message, variables: { filter: { @@ -25,6 +31,18 @@ export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperation subject: true, text: true, receivedAt: true, + messageThread: { + id: true, + subscribers: isSubscribersEnabled + ? { + workspaceMember: { + id: true, + name: true, + avatarUrl: true, + }, + } + : undefined, + }, messageParticipants: { id: true, role: true, diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx index 09c98810b..a03223780 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx @@ -1,5 +1,6 @@ import styled from '@emotion/styled'; -import { useRecoilCallback } from 'recoil'; +import { useEffect, useMemo } from 'react'; +import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader'; import { EmailLoader } from '@/activities/emails/components/EmailLoader'; @@ -8,8 +9,8 @@ import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMe import { IntermediaryMessages } from '@/activities/emails/right-drawer/components/IntermediaryMessages'; import { useRightDrawerEmailThread } from '@/activities/emails/right-drawer/hooks/useRightDrawerEmailThread'; import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState'; -import { EmailThreadMessage as EmailThreadMessageType } from '@/activities/emails/types/EmailThreadMessage'; import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; +import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; const StyledContainer = styled.div` @@ -22,21 +23,31 @@ const StyledContainer = styled.div` position: relative; `; -const getVisibleMessages = (messages: EmailThreadMessageType[]) => - messages.filter(({ messageParticipants }) => { - const from = messageParticipants.find( - (participant) => participant.role === 'from', - ); - const receivers = messageParticipants.filter( - (participant) => participant.role !== 'from', - ); - return from && receivers.length > 0; - }); - export const RightDrawerEmailThread = () => { + const setMessageThread = useSetRecoilState(messageThreadState); + const { thread, messages, fetchMoreMessages, loading } = useRightDrawerEmailThread(); + const visibleMessages = useMemo(() => { + return messages.filter(({ messageParticipants }) => { + const from = messageParticipants.find( + (participant) => participant.role === 'from', + ); + const receivers = messageParticipants.filter( + (participant) => participant.role !== 'from', + ); + return from && receivers.length > 0; + }); + }, [messages]); + + useEffect(() => { + if (!visibleMessages[0]?.messageThread) { + return; + } + setMessageThread(visibleMessages[0]?.messageThread); + }); + const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener( RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID, ); @@ -60,7 +71,6 @@ export const RightDrawerEmailThread = () => { return null; } - const visibleMessages = getVisibleMessages(messages); const visibleMessagesCount = visibleMessages.length; const is5OrMoreMessages = visibleMessagesCount >= 5; const firstMessages = visibleMessages.slice( diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts index 47c30f4d0..da57c1c4d 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts @@ -3,12 +3,13 @@ import { useRecoilValue } from 'recoil'; import { fetchAllThreadMessagesOperationSignatureFactory } from '@/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory'; import { EmailThread } from '@/activities/emails/types/EmailThread'; -import { EmailThreadMessage as EmailThreadMessageType } from '@/activities/emails/types/EmailThreadMessage'; +import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; export const useRightDrawerEmailThread = () => { const viewableRecordId = useRecoilValue(viewableRecordIdState); @@ -20,19 +21,26 @@ export const useRightDrawerEmailThread = () => { recordGqlFields: { id: true, }, - onCompleted: (record) => upsertRecords([record]), + onCompleted: (record) => { + upsertRecords([record]); + }, }); + const isMessageThreadSubscribersEnabled = useIsFeatureEnabled( + 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED', + ); + const FETCH_ALL_MESSAGES_OPERATION_SIGNATURE = fetchAllThreadMessagesOperationSignatureFactory({ messageThreadId: viewableRecordId, + isSubscribersEnabled: isMessageThreadSubscribersEnabled, }); const { records: messages, loading, fetchMoreRecords, - } = useFindManyRecords({ + } = useFindManyRecords({ limit: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.limit, filter: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.filter, objectNameSingular: diff --git a/packages/twenty-front/src/modules/activities/emails/types/EmailThreadMessage.ts b/packages/twenty-front/src/modules/activities/emails/types/EmailThreadMessage.ts index 01f6555b9..80584c074 100644 --- a/packages/twenty-front/src/modules/activities/emails/types/EmailThreadMessage.ts +++ b/packages/twenty-front/src/modules/activities/emails/types/EmailThreadMessage.ts @@ -1,4 +1,5 @@ import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant'; +import { MessageThread } from '@/activities/emails/types/MessageThread'; export type EmailThreadMessage = { id: string; @@ -7,5 +8,6 @@ export type EmailThreadMessage = { subject: string; messageThreadId: string; messageParticipants: EmailThreadMessageParticipant[]; + messageThread: MessageThread; __typename: 'EmailThreadMessage'; }; diff --git a/packages/twenty-front/src/modules/activities/emails/types/MessageThread.ts b/packages/twenty-front/src/modules/activities/emails/types/MessageThread.ts new file mode 100644 index 000000000..b853f8328 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/emails/types/MessageThread.ts @@ -0,0 +1,6 @@ +import { MessageThreadSubscriber } from '@/activities/emails/types/MessageThreadSubscriber'; + +export type MessageThread = { + id: string; + subscribers?: MessageThreadSubscriber[]; +}; diff --git a/packages/twenty-front/src/modules/activities/emails/types/MessageThreadSubscriber.ts b/packages/twenty-front/src/modules/activities/emails/types/MessageThreadSubscriber.ts new file mode 100644 index 000000000..7f13c279d --- /dev/null +++ b/packages/twenty-front/src/modules/activities/emails/types/MessageThreadSubscriber.ts @@ -0,0 +1,7 @@ +import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; + +export type MessageThreadSubscriber = { + __typename: 'MessageThreadSubscriber'; + id: string; + workspaceMember: WorkspaceMember; +}; diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/MessageThreadSubscribersTopBar.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/MessageThreadSubscribersTopBar.tsx new file mode 100644 index 000000000..5c85aece3 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/right-drawer/components/MessageThreadSubscribersTopBar.tsx @@ -0,0 +1,40 @@ +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; + +import { EmailThreadMembersChip } from '@/activities/emails/components/EmailThreadMembersChip'; +import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { isDefined } from 'twenty-ui'; + +const StyledButtonContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; + padding: ${({ theme }) => theme.spacing(2)}; +`; + +export const MessageThreadSubscribersTopBar = () => { + const isMessageThreadSubscriberEnabled = useIsFeatureEnabled( + 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED', + ); + + const messageThread = useRecoilValue(messageThreadState); + + const numberOfSubscribers = messageThread?.subscribers?.length ?? 0; + + const shouldShowMembersChip = numberOfSubscribers > 0; + + if ( + !isMessageThreadSubscriberEnabled || + !isDefined(messageThread) || + !shouldShowMembersChip + ) { + return null; + } + + return ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts index 1148c99dd..f0b76f473 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts @@ -27,4 +27,5 @@ export enum CoreObjectNameSingular { ViewSort = 'viewSort', Webhook = 'webhook', WorkspaceMember = 'workspaceMember', + MessageThreadSubscriber = 'messageThreadSubscriber', } diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx index 537c2fd08..2c609e0e3 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx @@ -3,13 +3,12 @@ import { SingleEntitySelectMenuItems, SingleEntitySelectMenuItemsProps, } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems'; +import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch'; import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { isDefined } from '~/utils/isDefined'; -import { useEntitySelectSearch } from '../hooks/useEntitySelectSearch'; - export type SingleEntitySelectMenuItemsWithSearchProps = { excludedRelationRecordIds?: string[]; onCreate?: ((searchInput?: string) => void) | (() => void); diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts index 7712852c8..129d93f1d 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts @@ -3,6 +3,7 @@ import { useRecoilState } from 'recoil'; import { useDropdownStates } from '@/ui/layout/dropdown/hooks/internal/useDropdownStates'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId'; +import { useCallback } from 'react'; import { isDefined } from '~/utils/isDefined'; export const useDropdown = (dropdownId?: string) => { @@ -27,10 +28,10 @@ export const useDropdown = (dropdownId?: string) => { const [isDropdownOpen, setIsDropdownOpen] = useRecoilState(isDropdownOpenState); - const closeDropdown = () => { + const closeDropdown = useCallback(() => { goBackToPreviousHotkeyScope(); setIsDropdownOpen(false); - }; + }, [goBackToPreviousHotkeyScope, setIsDropdownOpen]); const openDropdown = () => { setIsDropdownOpen(true); diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx index 18eaf4aa4..529b0d1f7 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx @@ -25,6 +25,7 @@ import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState'; import { rightDrawerPageState } from '../states/rightDrawerPageState'; import { RightDrawerHotkeyScope } from '../types/RightDrawerHotkeyScope'; +import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent'; import { RightDrawerRouter } from './RightDrawerRouter'; const StyledContainer = styled(motion.div)` @@ -47,6 +48,41 @@ const StyledRightDrawer = styled.div` `; export const RightDrawer = () => { + const theme = useTheme(); + + const animationVariants = { + fullScreen: { + x: '0%', + width: '100%', + height: '100%', + bottom: '0', + top: '0', + }, + normal: { + x: '0%', + width: theme.rightDrawerWidth, + height: '100%', + bottom: '0', + top: '0', + }, + closed: { + x: '100%', + width: '0', + height: '100%', + bottom: '0', + top: 'auto', + }, + minimized: { + x: '0%', + width: 220, + height: 41, + bottom: '0', + top: 'auto', + }, + }; + + type RightDrawerAnimationVariant = keyof typeof animationVariants; + const [isRightDrawerOpen, setIsRightDrawerOpen] = useRecoilState( isRightDrawerOpenState, ); @@ -82,6 +118,8 @@ export const RightDrawer = () => { if (isRightDrawerOpen && !isRightDrawerMinimized) { set(rightDrawerCloseEventState, event); closeRightDrawer(); + + emitRightDrawerCloseEvent(); } }, [closeRightDrawer], @@ -89,11 +127,8 @@ export const RightDrawer = () => { mode: ClickOutsideMode.comparePixels, }); - const theme = useTheme(); - useScopedHotkeys( [Key.Escape], - () => { closeRightDrawer(); }, @@ -103,56 +138,27 @@ export const RightDrawer = () => { const isMobile = useIsMobile(); - const rightDrawerWidth = isRightDrawerOpen - ? isMobile - ? '100%' - : theme.rightDrawerWidth - : '0'; + const targetVariantForAnimation: RightDrawerAnimationVariant = + !isRightDrawerOpen + ? 'closed' + : isRightDrawerMinimized + ? 'minimized' + : isMobile + ? 'fullScreen' + : 'normal'; + + const handleAnimationComplete = () => { + setIsRightDrawerAnimationCompleted(isRightDrawerOpen); + }; if (!isDefined(rightDrawerPage)) { return <>; } - const variants = { - fullScreen: { - x: '0%', - }, - normal: { - x: '0%', - width: rightDrawerWidth, - }, - closed: { - x: '100%', - }, - minimized: { - x: '0%', - width: 'auto', - height: 'auto', - bottom: '0', - top: 'auto', - }, - }; - const handleAnimationComplete = () => { - setIsRightDrawerAnimationCompleted(isRightDrawerOpen); - }; - return ( , - topBar: , - }, - [RightDrawerPages.ViewCalendarEvent]: { - page: , - topBar: , - }, - [RightDrawerPages.ViewRecord]: { - page: , - topBar: , - }, - [RightDrawerPages.Copilot]: { - page: , - topBar: , - }, +const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = { + [RightDrawerPages.ViewEmailThread]: , + [RightDrawerPages.ViewCalendarEvent]: , + [RightDrawerPages.ViewRecord]: , + [RightDrawerPages.Copilot]: , }; export const RightDrawerRouter = () => { const [rightDrawerPage] = useRecoilState(rightDrawerPageState); - const { topBar = null, page = null } = rightDrawerPage - ? RIGHT_DRAWER_PAGES_CONFIG[rightDrawerPage] - : {}; + const rightDrawerPageComponent = isDefined(rightDrawerPage) ? ( + RIGHT_DRAWER_PAGES_CONFIG[rightDrawerPage] + ) : ( + <> + ); const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState); return ( - {topBar} + {!isRightDrawerMinimized && ( - {page} + + {rightDrawerPageComponent} + )} ); diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx index 64eddfd9b..9176af203 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx @@ -8,16 +8,19 @@ import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShow import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { RightDrawerTopBarCloseButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton'; +import { RightDrawerTopBarDropdownButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarDropdownButton'; import { RightDrawerTopBarExpandButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton'; import { RightDrawerTopBarMinimizeButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton'; import { StyledRightDrawerTopBar } from '@/ui/layout/right-drawer/components/StyledRightDrawerTopBar'; import { RIGHT_DRAWER_PAGE_ICONS } from '@/ui/layout/right-drawer/constants/RightDrawerPageIcons'; import { RIGHT_DRAWER_PAGE_TITLES } from '@/ui/layout/right-drawer/constants/RightDrawerPageTitles'; import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState'; +import { rightDrawerPageState } from '@/ui/layout/right-drawer/states/rightDrawerPageState'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; const StyledTopBarWrapper = styled.div` + align-items: center; display: flex; `; @@ -40,9 +43,11 @@ const StyledMinimizeTopBarIcon = styled.div` display: flex; `; -export const RightDrawerTopBar = ({ page }: { page: RightDrawerPages }) => { +export const RightDrawerTopBar = () => { const isMobile = useIsMobile(); + const rightDrawerPage = useRecoilValue(rightDrawerPageState); + const [isRightDrawerMinimized, setIsRightDrawerMinimized] = useRecoilState( isRightDrawerMinimizedState, ); @@ -57,8 +62,6 @@ export const RightDrawerTopBar = ({ page }: { page: RightDrawerPages }) => { const { getIcon } = useIcons(); - const PageIcon = getIcon(RIGHT_DRAWER_PAGE_ICONS[page]); - const viewableRecordNameSingular = useRecoilValue( viewableRecordNameSingularState, ); @@ -69,14 +72,21 @@ export const RightDrawerTopBar = ({ page }: { page: RightDrawerPages }) => { objectNameSingular: viewableRecordNameSingular ?? 'company', }); + if (!rightDrawerPage) { + return null; + } + + const PageIcon = getIcon(RIGHT_DRAWER_PAGE_ICONS[rightDrawerPage]); + const ObjectIcon = getIcon(objectMetadataItem.icon); const label = - page === RightDrawerPages.ViewRecord + rightDrawerPage === RightDrawerPages.ViewRecord ? objectMetadataItem.labelSingular - : RIGHT_DRAWER_PAGE_TITLES[page]; + : RIGHT_DRAWER_PAGE_TITLES[rightDrawerPage]; - const Icon = page === RightDrawerPages.ViewRecord ? ObjectIcon : PageIcon; + const Icon = + rightDrawerPage === RightDrawerPages.ViewRecord ? ObjectIcon : PageIcon; return ( { )} + {!isMobile && !isRightDrawerMinimized && ( )} diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarDropdownButton.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarDropdownButton.tsx new file mode 100644 index 000000000..823d6bdbf --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarDropdownButton.tsx @@ -0,0 +1,27 @@ +import { MessageThreadSubscribersTopBar } from '@/activities/right-drawer/components/MessageThreadSubscribersTopBar'; +import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState'; +import { rightDrawerPageState } from '@/ui/layout/right-drawer/states/rightDrawerPageState'; +import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage'; +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; +import { useRecoilState } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +const RIGHT_DRAWER_TOP_BAR_DROPDOWN_BUTTON_CONFIG: ComponentByRightDrawerPage = + { + [RightDrawerPages.ViewEmailThread]: , + }; + +export const RightDrawerTopBarDropdownButton = () => { + const [isRightDrawerMinimized] = useRecoilState(isRightDrawerMinimizedState); + + const [rightDrawerPage] = useRecoilState(rightDrawerPageState); + + if (isRightDrawerMinimized || !isDefined(rightDrawerPage)) { + return null; + } + + const dropdownButtonComponent = + RIGHT_DRAWER_TOP_BAR_DROPDOWN_BUTTON_CONFIG[rightDrawerPage]; + + return dropdownButtonComponent ?? <>; +}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useListenRightDrawerClose.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useListenRightDrawerClose.ts new file mode 100644 index 000000000..29fc73d53 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useListenRightDrawerClose.ts @@ -0,0 +1,12 @@ +import { RIGHT_DRAWER_CLOSE_EVENT_NAME } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent'; +import { useEffect } from 'react'; + +export const useListenRightDrawerClose = (callback: () => void) => { + useEffect(() => { + window.addEventListener(RIGHT_DRAWER_CLOSE_EVENT_NAME, callback); + + return () => { + window.removeEventListener(RIGHT_DRAWER_CLOSE_EVENT_NAME, callback); + }; + }, [callback]); +}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/states/messageThreadState.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/states/messageThreadState.ts new file mode 100644 index 000000000..2e1febcf6 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/states/messageThreadState.ts @@ -0,0 +1,8 @@ +import { createState } from 'twenty-ui'; + +import { MessageThread } from '@/activities/emails/types/MessageThread'; + +export const messageThreadState = createState({ + key: 'messageThreadState', + defaultValue: null, +}); diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/states/rightDrawerHeaderDropdownButtonState.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/states/rightDrawerHeaderDropdownButtonState.ts new file mode 100644 index 000000000..a0f2a8496 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/states/rightDrawerHeaderDropdownButtonState.ts @@ -0,0 +1,9 @@ +import { createState } from 'twenty-ui'; + +import { RightDrawerTopBarDropdownButtons } from '@/ui/layout/right-drawer/types/RightDrawerTopBarDropdownButtons'; + +export const rightDrawerTopBarDropdownButtonState = + createState({ + key: 'rightDrawerTopBarDropdownButtonState', + defaultValue: null, + }); diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/ComponentByRightDrawerPage.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/ComponentByRightDrawerPage.ts new file mode 100644 index 000000000..bcef77e8c --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/ComponentByRightDrawerPage.ts @@ -0,0 +1,5 @@ +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; + +export type ComponentByRightDrawerPage = { + [componentName in RightDrawerPages]?: JSX.Element; +}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerTopBarDropdownButtons.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerTopBarDropdownButtons.ts new file mode 100644 index 000000000..b76d5b399 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerTopBarDropdownButtons.ts @@ -0,0 +1,3 @@ +export enum RightDrawerTopBarDropdownButtons { + EmailThreadSubscribers = 'EmailThreadSubscribers', +} diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent.ts new file mode 100644 index 000000000..e147a2f2f --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent.ts @@ -0,0 +1,5 @@ +export const RIGHT_DRAWER_CLOSE_EVENT_NAME = 'right-drawer-close'; + +export const emitRightDrawerCloseEvent = () => { + window.dispatchEvent(new CustomEvent(RIGHT_DRAWER_CLOSE_EVENT_NAME)); +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx index a85712807..5ff957ea0 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx @@ -1,5 +1,5 @@ -import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react'; import { useTheme } from '@emotion/react'; +import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react'; import { IconChevronRight, IconComponent } from 'twenty-ui'; import { LightIconButtonProps } from '@/ui/input/button/components/LightIconButton'; @@ -76,7 +76,6 @@ export const MenuItem = ({ )} - {hasSubMenu && ( ; + Icon: IconComponent; + accent?: LightIconButtonProps['accent']; + onClick?: (event: MouseEvent) => void; +}; + +export type MenuItemAvatarProps = { + accent?: MenuItemAccent; + className?: string; + iconButtons?: MenuItemIconButton[]; + isIconDisplayedOnHoverOnly?: boolean; + isTooltipOpen?: boolean; + avatar?: Pick< + AvatarProps, + 'avatarUrl' | 'placeholderColorSeed' | 'placeholder' | 'size' | 'type' + > | null; + onClick?: (event: MouseEvent) => void; + onMouseEnter?: (event: MouseEvent) => void; + onMouseLeave?: (event: MouseEvent) => void; + testId?: string; + text: string; + hasSubMenu?: boolean; +}; + +// TODO: merge with MenuItem +export const MenuItemAvatar = ({ + accent = 'default', + className, + iconButtons, + isIconDisplayedOnHoverOnly = true, + onClick, + onMouseEnter, + onMouseLeave, + testId, + avatar, + hasSubMenu = false, + text, +}: MenuItemAvatarProps) => { + const theme = useTheme(); + const showIconButtons = Array.isArray(iconButtons) && iconButtons.length > 0; + + const handleMenuItemClick = (event: MouseEvent) => { + if (!onClick) return; + event.preventDefault(); + event.stopPropagation(); + + onClick?.(event); + }; + + return ( + + + {isDefined(avatar) && ( + + )} + + +
+ {showIconButtons && ( + + )} +
+ {hasSubMenu && ( + + )} +
+ ); +}; diff --git a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts index bbcca44a7..53f29d4b2 100644 --- a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts +++ b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts @@ -7,4 +7,5 @@ export type FeatureFlagKey = | 'IS_STRIPE_INTEGRATION_ENABLED' | 'IS_FUNCTION_SETTINGS_ENABLED' | 'IS_COPILOT_ENABLED' - | 'IS_CRM_MIGRATION_ENABLED'; + | 'IS_CRM_MIGRATION_ENABLED' + | 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED'; diff --git a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts index 57cf1fa56..b701093e7 100644 --- a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts +++ b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts @@ -15,6 +15,7 @@ import { seedConnectedAccount } from 'src/database/typeorm-seeds/workspace/conne import { seedMessageChannelMessageAssociation } from 'src/database/typeorm-seeds/workspace/message-channel-message-associations'; import { seedMessageChannel } from 'src/database/typeorm-seeds/workspace/message-channels'; import { seedMessageParticipant } from 'src/database/typeorm-seeds/workspace/message-participants'; +import { seedMessageThreadSubscribers } from 'src/database/typeorm-seeds/workspace/message-thread-subscribers'; import { seedMessageThread } from 'src/database/typeorm-seeds/workspace/message-threads'; import { seedMessage } from 'src/database/typeorm-seeds/workspace/messages'; import { seedOpportunity } from 'src/database/typeorm-seeds/workspace/opportunities'; @@ -22,6 +23,8 @@ import { seedPeople } from 'src/database/typeorm-seeds/workspace/people'; import { seedWorkspaceMember } from 'src/database/typeorm-seeds/workspace/workspace-members'; import { rawDataSource } from 'src/database/typeorm/raw/raw.datasource'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { CacheStorageService } from 'src/engine/integrations/cache-storage/cache-storage.service'; import { InjectCacheStorage } from 'src/engine/integrations/cache-storage/decorators/cache-storage.decorator'; import { CacheStorageNamespace } from 'src/engine/integrations/cache-storage/types/cache-storage-namespace.enum'; @@ -117,6 +120,11 @@ export class DataSeedWorkspaceCommand extends CommandRunner { return acc; }, {}); + const featureFlagRepository = + workspaceDataSource.getRepository('featureFlag'); + + const featureFlags = await featureFlagRepository.find({}); + await workspaceDataSource.transaction( async (entityManager: EntityManager) => { await seedCompanies(entityManager, dataSourceMetadata.schema); @@ -134,6 +142,21 @@ export class DataSeedWorkspaceCommand extends CommandRunner { entityManager, dataSourceMetadata.schema, ); + + const isMessageThreadSubscriberEnabled = featureFlags.some( + (featureFlag) => + featureFlag.key === + FeatureFlagKey.IsMessageThreadSubscriberEnabled && + featureFlag.value === true, + ); + + if (isMessageThreadSubscriberEnabled) { + await seedMessageThreadSubscribers( + entityManager, + dataSourceMetadata.schema, + ); + } + await seedMessage(entityManager, dataSourceMetadata.schema); await seedMessageChannel( entityManager, diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts index 0fec06b89..dd0be3fa8 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts @@ -60,6 +60,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: false, }, + { + key: FeatureFlagKey.IsMessageThreadSubscriberEnabled, + workspaceId: workspaceId, + value: false, + }, ]) .execute(); }; diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/message-thread-subscribers.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/message-thread-subscribers.ts new file mode 100644 index 000000000..637446cd9 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/message-thread-subscribers.ts @@ -0,0 +1,103 @@ +import { EntityManager } from 'typeorm'; + +const tableName = 'messageThreadSubscriber'; + +export const DEV_SEED_MESSAGE_THREAD_SUBSCRIBERS_IDS = { + MESSAGE_THREAD_SUBSCRIBER_1: '20202020-cc69-44ef-a82c-600c0dbf39ba', + MESSAGE_THREAD_SUBSCRIBER_2: '20202020-d80e-4a13-b10b-72ba09082668', + MESSAGE_THREAD_SUBSCRIBER_3: '20202020-e6ec-4c8a-b431-0901eaf395a9', + MESSAGE_THREAD_SUBSCRIBER_4: '20202020-1455-4c57-afaf-dd5dc086361d', + MESSAGE_THREAD_SUBSCRIBER_5: '20202020-f79e-40dd-bd06-c36e6abb4678', + MESSAGE_THREAD_SUBSCRIBER_6: '20202020-3ec3-4fe3-8997-b76aa0bfa408', + MESSAGE_THREAD_SUBSCRIBER_7: '20202020-c21e-4ec2-873b-de4264d89025', +}; + +export const DEV_SEED_MESSAGE_THREAD_IDS = { + MESSAGE_THREAD_1: '20202020-8bfa-453b-b99b-bc435a7d4da8', + MESSAGE_THREAD_2: '20202020-634a-4fde-aa7c-28a0eaf203ca', + MESSAGE_THREAD_3: '20202020-1b56-4f10-a2fa-2ccaddf81f6c', + MESSAGE_THREAD_4: '20202020-d51c-485a-b1b6-ed7c63e05d72', +}; + +export const DEV_SEED_USER_IDS = { + TIM: '20202020-0687-4c41-b707-ed1bfca972a7', + PHIL: '20202020-1553-45c6-a028-5a9064cce07f', + JONY: '20202020-77d5-4cb6-b60a-f4a835a85d61', +}; + +export const seedMessageThreadSubscribers = async ( + entityManager: EntityManager, + schemaName: string, +) => { + await entityManager + .createQueryBuilder() + .insert() + .into(`${schemaName}.${tableName}`, [ + 'id', + 'createdAt', + 'updatedAt', + 'deletedAt', + 'messageThreadId', + 'workspaceMemberId', + ]) + .orIgnore() + .values([ + { + id: DEV_SEED_MESSAGE_THREAD_SUBSCRIBERS_IDS.MESSAGE_THREAD_SUBSCRIBER_1, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_1, + workspaceMemberId: DEV_SEED_USER_IDS.PHIL, + }, + { + id: DEV_SEED_MESSAGE_THREAD_SUBSCRIBERS_IDS.MESSAGE_THREAD_SUBSCRIBER_2, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_1, + workspaceMemberId: DEV_SEED_USER_IDS.JONY, + }, + { + id: DEV_SEED_MESSAGE_THREAD_SUBSCRIBERS_IDS.MESSAGE_THREAD_SUBSCRIBER_3, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_2, + workspaceMemberId: DEV_SEED_USER_IDS.TIM, + }, + { + id: DEV_SEED_MESSAGE_THREAD_SUBSCRIBERS_IDS.MESSAGE_THREAD_SUBSCRIBER_4, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_3, + workspaceMemberId: DEV_SEED_USER_IDS.JONY, + }, + { + id: DEV_SEED_MESSAGE_THREAD_SUBSCRIBERS_IDS.MESSAGE_THREAD_SUBSCRIBER_5, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_4, + workspaceMemberId: DEV_SEED_USER_IDS.TIM, + }, + { + id: DEV_SEED_MESSAGE_THREAD_SUBSCRIBERS_IDS.MESSAGE_THREAD_SUBSCRIBER_6, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_4, + workspaceMemberId: DEV_SEED_USER_IDS.PHIL, + }, + { + id: DEV_SEED_MESSAGE_THREAD_SUBSCRIBERS_IDS.MESSAGE_THREAD_SUBSCRIBER_7, + createdAt: new Date(), + updatedAt: new Date(), + deletedAt: null, + messageThreadId: DEV_SEED_MESSAGE_THREAD_IDS.MESSAGE_THREAD_4, + workspaceMemberId: DEV_SEED_USER_IDS.JONY, + }, + ]) + .execute(); +}; diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/message-threads.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/message-threads.ts index 42a2f14fc..a7d8ea1df 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/workspace/message-threads.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/message-threads.ts @@ -7,6 +7,7 @@ export const DEV_SEED_MESSAGE_THREAD_IDS = { MESSAGE_THREAD_2: '20202020-634a-4fde-aa7c-28a0eaf203ca', MESSAGE_THREAD_3: '20202020-1b56-4f10-a2fa-2ccaddf81f6c', MESSAGE_THREAD_4: '20202020-d51c-485a-b1b6-ed7c63e05d72', + MESSAGE_THREAD_5: '20202020-3f74-492d-a101-2a70f50a1645', }; export const seedMessageThread = async ( diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 05bda801f..fd30ef5bd 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -10,4 +10,5 @@ export enum FeatureFlagKey { IsFreeAccessEnabled = 'IS_FREE_ACCESS_ENABLED', IsFunctionSettingsEnabled = 'IS_FUNCTION_SETTINGS_ENABLED', IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED', + IsMessageThreadSubscriberEnabled = 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED', } diff --git a/packages/twenty-server/src/engine/core-modules/messaging/dtos/timeline-thread.dto.ts b/packages/twenty-server/src/engine/core-modules/messaging/dtos/timeline-thread.dto.ts index 34b94a056..431f00d5a 100644 --- a/packages/twenty-server/src/engine/core-modules/messaging/dtos/timeline-thread.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/messaging/dtos/timeline-thread.dto.ts @@ -1,4 +1,4 @@ -import { ObjectType, Field, registerEnumType } from '@nestjs/graphql'; +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { TimelineThreadParticipant } from 'src/engine/core-modules/messaging/dtos/timeline-thread-participant.dto'; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts index 07e5efc09..de3758b26 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/commands/add-standard-id.command.ts @@ -64,6 +64,7 @@ export class AddStandardIdCommand extends CommandRunner { IS_FREE_ACCESS_ENABLED: false, IS_FUNCTION_SETTINGS_ENABLED: false, IS_WORKFLOW_ENABLED: false, + IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED: false, }, ); const standardFieldMetadataCollection = this.standardFieldFactory.create( @@ -84,6 +85,7 @@ export class AddStandardIdCommand extends CommandRunner { IS_FREE_ACCESS_ENABLED: false, IS_FUNCTION_SETTINGS_ENABLED: false, IS_WORKFLOW_ENABLED: false, + IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED: false, }, ); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 73de80046..c1c13755b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -244,6 +244,12 @@ export const MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS = { export const MESSAGE_THREAD_STANDARD_FIELD_IDS = { messages: '20202020-3115-404f-aade-e1154b28e35a', messageChannelMessageAssociations: '20202020-314e-40a4-906d-a5d5d6c285f6', + messageThreadSubscribers: '20202020-3b3b-4b3b-8b3b-7f8d6a1d7d5b', +}; + +export const MESSAGE_THREAD_SUBSCRIBER_STANDARD_FIELD_IDS = { + messageThread: '20202020-2c8f-4f3e-8b9a-7f8d6a1c7d5b', + workspaceMember: '20202020-7f7b-4b3b-8b3b-7f8d6a1d7d5a', }; export const MESSAGE_STANDARD_FIELD_IDS = { @@ -418,6 +424,7 @@ export const WORKSPACE_MEMBER_STANDARD_FIELD_IDS = { calendarEventParticipants: '20202020-0dbc-4841-9ce1-3e793b5b3512', timelineActivities: '20202020-e15b-47b8-94fe-8200e3c66615', auditLogs: '20202020-2f54-4739-a5e2-99563385e83d', + messageThreadSubscribers: '20202020-4b3b-4b3b-9b3b-3b3b3b3b3b3b', timeZone: '20202020-2d33-4c21-a86e-5943b050dd54', dateFormat: '20202020-af13-4e11-b1e7-b8cf5ea13dc0', timeFormat: '20202020-8acb-4cf8-a851-a6ed443c8d81', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts index d0f4042b9..7be15073d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids.ts @@ -25,6 +25,7 @@ export const STANDARD_OBJECT_IDS = { messageChannel: '20202020-fe8c-40bc-a681-b80b771449b7', messageParticipant: '20202020-a433-4456-aa2d-fd9cb26b774a', messageThread: '20202020-849a-4c3e-84f5-a25a7d802271', + messageThreadSubscriber: '20202020-4b3b-4b3b-8b3b-3b3b3b3b3b3a', message: '20202020-3f6b-4425-80ab-e468899ab4b2', note: '20202020-0b00-45cd-b6f6-6cd806fc6804', noteTarget: '20202020-fff0-4b44-be82-bda313884400', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts index 9a42b4dd7..95c148669 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts @@ -14,6 +14,7 @@ import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/f import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; +import { MessageThreadSubscriberWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity'; import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity'; import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/note-target.workspace-entity'; @@ -65,6 +66,7 @@ export const standardObjectMetadataDefinitions = [ WorkflowVersionWorkspaceEntity, WorkspaceMemberWorkspaceEntity, MessageThreadWorkspaceEntity, + MessageThreadSubscriberWorkspaceEntity, MessageWorkspaceEntity, MessageChannelWorkspaceEntity, MessageParticipantWorkspaceEntity, diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity.ts new file mode 100644 index 000000000..05f052c42 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity.ts @@ -0,0 +1,58 @@ +import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; + +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; +import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; +import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator'; +import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; +import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; +import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator'; +import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; +import { MESSAGE_THREAD_SUBSCRIBER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; +import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; + +@WorkspaceEntity({ + standardId: STANDARD_OBJECT_IDS.messageThreadSubscriber, + namePlural: 'messageThreadSubscriber', + labelSingular: 'Message Thread Subscriber', + labelPlural: 'Message Threads Subscribers', + description: 'Message Thread Subscribers', + icon: 'IconPerson', +}) +@WorkspaceIsNotAuditLogged() +@WorkspaceIsSystem() +@WorkspaceGate({ + featureFlag: FeatureFlagKey.IsMessageThreadSubscriberEnabled, +}) +export class MessageThreadSubscriberWorkspaceEntity extends BaseWorkspaceEntity { + @WorkspaceRelation({ + standardId: MESSAGE_THREAD_SUBSCRIBER_STANDARD_FIELD_IDS.messageThread, + type: RelationMetadataType.MANY_TO_ONE, + label: 'Message Thread', + description: 'Message Thread', + icon: 'IconMessage', + inverseSideFieldKey: 'subscribers', + inverseSideTarget: () => MessageThreadWorkspaceEntity, + }) + messageThread: Relation; + + @WorkspaceJoinColumn('messageThread') + messageThreadId: string; + + @WorkspaceRelation({ + standardId: MESSAGE_THREAD_SUBSCRIBER_STANDARD_FIELD_IDS.workspaceMember, + type: RelationMetadataType.MANY_TO_ONE, + label: 'Workspace Member', + description: 'Workspace Member that is part of the message thread', + icon: 'IconCircleUser', + inverseSideFieldKey: 'messageThreadSubscribers', + inverseSideTarget: () => WorkspaceMemberWorkspaceEntity, + }) + workspaceMember: Relation; + + @WorkspaceJoinColumn('workspaceMember') + workspaceMemberId: string; +} diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread.workspace-entity.ts index 64b81723f..46b9f5c02 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread.workspace-entity.ts @@ -1,18 +1,21 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { RelationMetadataType, RelationOnDeleteAction, } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; -import { MESSAGE_THREAD_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; -import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; +import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator'; import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; -import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; +import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; +import { MESSAGE_THREAD_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; +import { MessageThreadSubscriberWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity'; import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; @WorkspaceEntity({ @@ -38,6 +41,20 @@ export class MessageThreadWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceIsNullable() messages: Relation; + @WorkspaceRelation({ + standardId: MESSAGE_THREAD_STANDARD_FIELD_IDS.messageThreadSubscribers, + type: RelationMetadataType.ONE_TO_MANY, + label: 'Message Thread Subscribers', + description: 'Message Thread Subscribers', + icon: 'IconMessage', + inverseSideTarget: () => MessageThreadSubscriberWorkspaceEntity, + onDelete: RelationOnDeleteAction.CASCADE, + }) + @WorkspaceGate({ + featureFlag: FeatureFlagKey.IsMessageThreadSubscriberEnabled, + }) + subscribers: Relation; + @WorkspaceRelation({ standardId: MESSAGE_THREAD_STANDARD_FIELD_IDS.messageChannelMessageAssociations, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-thread.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-thread.service.ts index 35c575638..c59c456a2 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-thread.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-thread.service.ts @@ -4,16 +4,19 @@ import { EntityManager } from 'typeorm'; import { v4 } from 'uuid'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { MessageChannelMessageAssociationRepository } from 'src/modules/messaging/common/repositories/message-channel-message-association.repository'; import { MessageThreadRepository } from 'src/modules/messaging/common/repositories/message-thread.repository'; import { MessageRepository } from 'src/modules/messaging/common/repositories/message.repository'; import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; +import { MessageThreadSubscriberWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity'; import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity'; import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; @Injectable() export class MessagingMessageThreadService { constructor( + private readonly twentyORMManager: TwentyORMManager, @InjectObjectMetadataRepository( MessageChannelMessageAssociationWorkspaceEntity, ) @@ -24,6 +27,24 @@ export class MessagingMessageThreadService { private readonly messageThreadRepository: MessageThreadRepository, ) {} + public async saveMessageThreadMember( + messageThreadId: string, + workspaceMemberId: string, + ) { + const id = v4(); + + const messageThreadSubscriberRepository = + await this.twentyORMManager.getRepository( + 'messageThreadSubscriber', + ); + + await messageThreadSubscriberRepository.insert({ + id, + messageThreadId, + workspaceMemberId, + }); + } + public async saveMessageThreadOrReturnExistingMessageThread( headerMessageId: string, messageThreadExternalId: string, diff --git a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts index 212250153..83371beb3 100644 --- a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts @@ -2,6 +2,7 @@ import { registerEnumType } from '@nestjs/graphql'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { @@ -11,6 +12,7 @@ import { import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; +import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator'; import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; @@ -26,6 +28,7 @@ import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/com import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; +import { MessageThreadSubscriberWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity'; import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity'; import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; @@ -170,6 +173,20 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { }) favorites: Relation; + @WorkspaceRelation({ + standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.messageThreadSubscribers, + type: RelationMetadataType.ONE_TO_MANY, + label: 'Message thread subscribers', + description: 'Message thread subscribers for this workspace member', + icon: 'IconMessage', + inverseSideTarget: () => MessageThreadSubscriberWorkspaceEntity, + onDelete: RelationOnDeleteAction.CASCADE, + }) + @WorkspaceGate({ + featureFlag: FeatureFlagKey.IsMessageThreadSubscriberEnabled, + }) + messageThreadSubscribers: Relation; + @WorkspaceRelation({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.accountOwnerForCompanies, type: RelationMetadataType.ONE_TO_MANY,