added "reply in gmail" button (#6754)

Issue https://github.com/twentyhq/twenty/issues/4217
This commit is contained in:
nitin
2024-08-28 20:17:55 +05:30
committed by GitHub
parent e7bbc7ddfc
commit 5deb0abe4d
5 changed files with 137 additions and 15 deletions

View File

@ -9,25 +9,46 @@ 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 { Button } from '@/ui/input/button/components/Button';
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';
import { IconArrowBackUp } from 'twenty-ui';
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
height: 85%;
justify-content: flex-start;
overflow-y: auto;
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 = () => {
const setMessageThread = useSetRecoilState(messageThreadState);
const { thread, messages, fetchMoreMessages, loading } =
useRightDrawerEmailThread();
const {
thread,
messages,
fetchMoreMessages,
threadLoading,
messageThreadExternalId,
connectedAccountHandle,
messageChannelLoading,
} = useRightDrawerEmailThread();
const visibleMessages = useMemo(() => {
return messages.filter(({ messageParticipants }) => {
@ -67,10 +88,6 @@ export const RightDrawerEmailThread = () => {
),
});
if (!thread) {
return null;
}
const visibleMessagesCount = visibleMessages.length;
const is5OrMoreMessages = visibleMessagesCount >= 5;
const firstMessages = visibleMessages.slice(
@ -83,9 +100,26 @@ export const RightDrawerEmailThread = () => {
const lastMessage = visibleMessages[visibleMessagesCount - 1];
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 (
<StyledContainer>
{loading ? (
{threadLoading ? (
<EmailLoader loadingText="Loading thread" />
) : (
<>
@ -110,11 +144,21 @@ export const RightDrawerEmailThread = () => {
isExpanded
/>
<CustomResolverFetchMoreLoader
loading={loading}
loading={threadLoading}
onLastRowVisible={fetchMoreMessages}
/>
</>
)}
{canReply && !messageChannelLoading ? (
<StyledButtonContainer>
<Button
onClick={handleReplyClick}
title="Reply (View in Gmail)"
Icon={IconArrowBackUp}
disabled={!canReply}
></Button>
</StyledButtonContainer>
) : null}
</StyledContainer>
);
};

View File

@ -50,7 +50,7 @@ describe('useRightDrawerEmailThread', () => {
expect(result.current.thread).toBeDefined();
expect(result.current.messages).toEqual(mockMessages);
expect(result.current.loading).toBeFalsy();
expect(result.current.threadLoading).toBeFalsy();
expect(result.current.fetchMoreMessages).toBeInstanceOf(Function);
});
});

View File

@ -1,9 +1,12 @@
import { useCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { fetchAllThreadMessagesOperationSignatureFactory } from '@/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory';
import { EmailThread } from '@/activities/emails/types/EmailThread';
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 { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
@ -14,6 +17,11 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
export const useRightDrawerEmailThread = () => {
const viewableRecordId = useRecoilValue(viewableRecordIdState);
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>({
objectNameSingular: CoreObjectNameSingular.MessageThread,
@ -38,8 +46,9 @@ export const useRightDrawerEmailThread = () => {
const {
records: messages,
loading,
loading: messagesLoading,
fetchMoreRecords,
hasNextPage,
} = useFindManyRecords<EmailThreadMessage>({
limit: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.limit,
filter: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.filter,
@ -51,15 +60,76 @@ export const useRightDrawerEmailThread = () => {
});
const fetchMoreMessages = useCallback(() => {
if (!loading) {
if (!messagesLoading && hasNextPage) {
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 {
thread,
messages,
loading,
messageThreadExternalId,
connectedAccountHandle,
threadLoading: messagesLoading,
messageChannelLoading,
fetchMoreMessages,
};
};

View File

@ -0,0 +1,7 @@
export type MessageChannelMessageAssociation = {
__typename: 'MessageChannelMessageAssociation';
id: string;
messageId: string;
messageChannelId: string;
messageThreadExternalId: string;
};

View File

@ -29,4 +29,5 @@ export enum CoreObjectNameSingular {
WorkspaceMember = 'workspaceMember',
MessageThreadSubscriber = 'messageThreadSubscriber',
Workflow = 'workflow',
MessageChannelMessageAssociation = 'messageChannelMessageAssociation',
}