added "reply in gmail" button (#6754)
Issue https://github.com/twentyhq/twenty/issues/4217
This commit is contained in:
@ -9,25 +9,46 @@ import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMe
|
|||||||
import { IntermediaryMessages } from '@/activities/emails/right-drawer/components/IntermediaryMessages';
|
import { IntermediaryMessages } from '@/activities/emails/right-drawer/components/IntermediaryMessages';
|
||||||
import { useRightDrawerEmailThread } from '@/activities/emails/right-drawer/hooks/useRightDrawerEmailThread';
|
import { useRightDrawerEmailThread } from '@/activities/emails/right-drawer/hooks/useRightDrawerEmailThread';
|
||||||
import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState';
|
import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState';
|
||||||
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
|
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
|
||||||
import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState';
|
import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState';
|
||||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||||
|
import { IconArrowBackUp } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 85%;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledButtonContainer = styled.div`
|
||||||
|
background: ${({ theme }) => theme.background.secondary};
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
height: 110px;
|
||||||
|
left: 0;
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(7)};
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(5)};
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
export const RightDrawerEmailThread = () => {
|
export const RightDrawerEmailThread = () => {
|
||||||
const setMessageThread = useSetRecoilState(messageThreadState);
|
const setMessageThread = useSetRecoilState(messageThreadState);
|
||||||
|
|
||||||
const { thread, messages, fetchMoreMessages, loading } =
|
const {
|
||||||
useRightDrawerEmailThread();
|
thread,
|
||||||
|
messages,
|
||||||
|
fetchMoreMessages,
|
||||||
|
threadLoading,
|
||||||
|
messageThreadExternalId,
|
||||||
|
connectedAccountHandle,
|
||||||
|
messageChannelLoading,
|
||||||
|
} = useRightDrawerEmailThread();
|
||||||
|
|
||||||
const visibleMessages = useMemo(() => {
|
const visibleMessages = useMemo(() => {
|
||||||
return messages.filter(({ messageParticipants }) => {
|
return messages.filter(({ messageParticipants }) => {
|
||||||
@ -67,10 +88,6 @@ export const RightDrawerEmailThread = () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!thread) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleMessagesCount = visibleMessages.length;
|
const visibleMessagesCount = visibleMessages.length;
|
||||||
const is5OrMoreMessages = visibleMessagesCount >= 5;
|
const is5OrMoreMessages = visibleMessagesCount >= 5;
|
||||||
const firstMessages = visibleMessages.slice(
|
const firstMessages = visibleMessages.slice(
|
||||||
@ -83,9 +100,26 @@ export const RightDrawerEmailThread = () => {
|
|||||||
const lastMessage = visibleMessages[visibleMessagesCount - 1];
|
const lastMessage = visibleMessages[visibleMessagesCount - 1];
|
||||||
const subject = visibleMessages[0]?.subject;
|
const subject = visibleMessages[0]?.subject;
|
||||||
|
|
||||||
|
const canReply = useMemo(() => {
|
||||||
|
return (
|
||||||
|
connectedAccountHandle && lastMessage && messageThreadExternalId != null
|
||||||
|
);
|
||||||
|
}, [connectedAccountHandle, lastMessage, messageThreadExternalId]);
|
||||||
|
|
||||||
|
const handleReplyClick = () => {
|
||||||
|
if (!canReply) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://mail.google.com/mail/?authuser=${connectedAccountHandle}#all/${messageThreadExternalId}`;
|
||||||
|
window.open(url, '_blank');
|
||||||
|
};
|
||||||
|
if (!thread) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
{loading ? (
|
{threadLoading ? (
|
||||||
<EmailLoader loadingText="Loading thread" />
|
<EmailLoader loadingText="Loading thread" />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -110,11 +144,21 @@ export const RightDrawerEmailThread = () => {
|
|||||||
isExpanded
|
isExpanded
|
||||||
/>
|
/>
|
||||||
<CustomResolverFetchMoreLoader
|
<CustomResolverFetchMoreLoader
|
||||||
loading={loading}
|
loading={threadLoading}
|
||||||
onLastRowVisible={fetchMoreMessages}
|
onLastRowVisible={fetchMoreMessages}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{canReply && !messageChannelLoading ? (
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<Button
|
||||||
|
onClick={handleReplyClick}
|
||||||
|
title="Reply (View in Gmail)"
|
||||||
|
Icon={IconArrowBackUp}
|
||||||
|
disabled={!canReply}
|
||||||
|
></Button>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
) : null}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -50,7 +50,7 @@ describe('useRightDrawerEmailThread', () => {
|
|||||||
|
|
||||||
expect(result.current.thread).toBeDefined();
|
expect(result.current.thread).toBeDefined();
|
||||||
expect(result.current.messages).toEqual(mockMessages);
|
expect(result.current.messages).toEqual(mockMessages);
|
||||||
expect(result.current.loading).toBeFalsy();
|
expect(result.current.threadLoading).toBeFalsy();
|
||||||
expect(result.current.fetchMoreMessages).toBeInstanceOf(Function);
|
expect(result.current.fetchMoreMessages).toBeInstanceOf(Function);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { fetchAllThreadMessagesOperationSignatureFactory } from '@/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory';
|
import { fetchAllThreadMessagesOperationSignatureFactory } from '@/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory';
|
||||||
import { EmailThread } from '@/activities/emails/types/EmailThread';
|
import { EmailThread } from '@/activities/emails/types/EmailThread';
|
||||||
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
|
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
|
||||||
|
|
||||||
|
import { MessageChannel } from '@/accounts/types/MessageChannel';
|
||||||
|
import { MessageChannelMessageAssociation } from '@/activities/emails/types/MessageChannelMessageAssociation';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
@ -14,6 +17,11 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
|||||||
export const useRightDrawerEmailThread = () => {
|
export const useRightDrawerEmailThread = () => {
|
||||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||||
const { upsertRecords } = useUpsertRecordsInStore();
|
const { upsertRecords } = useUpsertRecordsInStore();
|
||||||
|
const [lastMessageId, setLastMessageId] = useState<string | null>(null);
|
||||||
|
const [lastMessageChannelId, setLastMessageChannelId] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [isMessagesFetchComplete, setIsMessagesFetchComplete] = useState(false);
|
||||||
|
|
||||||
const { record: thread } = useFindOneRecord<EmailThread>({
|
const { record: thread } = useFindOneRecord<EmailThread>({
|
||||||
objectNameSingular: CoreObjectNameSingular.MessageThread,
|
objectNameSingular: CoreObjectNameSingular.MessageThread,
|
||||||
@ -38,8 +46,9 @@ export const useRightDrawerEmailThread = () => {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
records: messages,
|
records: messages,
|
||||||
loading,
|
loading: messagesLoading,
|
||||||
fetchMoreRecords,
|
fetchMoreRecords,
|
||||||
|
hasNextPage,
|
||||||
} = useFindManyRecords<EmailThreadMessage>({
|
} = useFindManyRecords<EmailThreadMessage>({
|
||||||
limit: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.limit,
|
limit: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.limit,
|
||||||
filter: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.filter,
|
filter: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.filter,
|
||||||
@ -51,15 +60,76 @@ export const useRightDrawerEmailThread = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fetchMoreMessages = useCallback(() => {
|
const fetchMoreMessages = useCallback(() => {
|
||||||
if (!loading) {
|
if (!messagesLoading && hasNextPage) {
|
||||||
fetchMoreRecords();
|
fetchMoreRecords();
|
||||||
|
} else if (!hasNextPage) {
|
||||||
|
setIsMessagesFetchComplete(true);
|
||||||
}
|
}
|
||||||
}, [fetchMoreRecords, loading]);
|
}, [fetchMoreRecords, messagesLoading, hasNextPage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messages.length > 0 && isMessagesFetchComplete) {
|
||||||
|
const lastMessage = messages[messages.length - 1];
|
||||||
|
setLastMessageId(lastMessage.id);
|
||||||
|
}
|
||||||
|
}, [messages, isMessagesFetchComplete]);
|
||||||
|
|
||||||
|
const { records: messageChannelMessageAssociationData } =
|
||||||
|
useFindManyRecords<MessageChannelMessageAssociation>({
|
||||||
|
filter: {
|
||||||
|
messageId: {
|
||||||
|
eq: lastMessageId ?? '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
objectNameSingular:
|
||||||
|
CoreObjectNameSingular.MessageChannelMessageAssociation,
|
||||||
|
recordGqlFields: {
|
||||||
|
id: true,
|
||||||
|
messageId: true,
|
||||||
|
messageChannelId: true,
|
||||||
|
messageThreadExternalId: true,
|
||||||
|
},
|
||||||
|
skip: !lastMessageId || !isMessagesFetchComplete,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (messageChannelMessageAssociationData.length > 0) {
|
||||||
|
setLastMessageChannelId(
|
||||||
|
messageChannelMessageAssociationData[0].messageChannelId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [messageChannelMessageAssociationData]);
|
||||||
|
|
||||||
|
const { records: messageChannelData, loading: messageChannelLoading } =
|
||||||
|
useFindManyRecords<MessageChannel>({
|
||||||
|
filter: {
|
||||||
|
id: {
|
||||||
|
eq: lastMessageChannelId ?? '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||||
|
recordGqlFields: {
|
||||||
|
id: true,
|
||||||
|
handle: true,
|
||||||
|
connectedAccountId: true,
|
||||||
|
},
|
||||||
|
skip: !lastMessageChannelId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageThreadExternalId =
|
||||||
|
messageChannelMessageAssociationData.length > 0
|
||||||
|
? messageChannelMessageAssociationData[0].messageThreadExternalId
|
||||||
|
: null;
|
||||||
|
const connectedAccountHandle =
|
||||||
|
messageChannelData.length > 0 ? messageChannelData[0].handle : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
thread,
|
thread,
|
||||||
messages,
|
messages,
|
||||||
loading,
|
messageThreadExternalId,
|
||||||
|
connectedAccountHandle,
|
||||||
|
threadLoading: messagesLoading,
|
||||||
|
messageChannelLoading,
|
||||||
fetchMoreMessages,
|
fetchMoreMessages,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
export type MessageChannelMessageAssociation = {
|
||||||
|
__typename: 'MessageChannelMessageAssociation';
|
||||||
|
id: string;
|
||||||
|
messageId: string;
|
||||||
|
messageChannelId: string;
|
||||||
|
messageThreadExternalId: string;
|
||||||
|
};
|
||||||
@ -29,4 +29,5 @@ export enum CoreObjectNameSingular {
|
|||||||
WorkspaceMember = 'workspaceMember',
|
WorkspaceMember = 'workspaceMember',
|
||||||
MessageThreadSubscriber = 'messageThreadSubscriber',
|
MessageThreadSubscriber = 'messageThreadSubscriber',
|
||||||
Workflow = 'workflow',
|
Workflow = 'workflow',
|
||||||
|
MessageChannelMessageAssociation = 'messageChannelMessageAssociation',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user