7242 error when displaying message threads with a large number of participants (#7251)

Closes #7242
This commit is contained in:
Raphaël Bosi
2024-09-25 16:53:18 +02:00
committed by GitHub
parent 7669b40543
commit 3d5ecc9c08
8 changed files with 77 additions and 35 deletions

View File

@ -1,5 +1,5 @@
import { useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useState } from 'react';
import { EmailThreadMessageBody } from '@/activities/emails/components/EmailThreadMessageBody'; import { EmailThreadMessageBody } from '@/activities/emails/components/EmailThreadMessageBody';
import { EmailThreadMessageBodyPreview } from '@/activities/emails/components/EmailThreadMessageBodyPreview'; import { EmailThreadMessageBodyPreview } from '@/activities/emails/components/EmailThreadMessageBodyPreview';
@ -30,6 +30,7 @@ const StyledThreadMessageBody = styled.div`
type EmailThreadMessageProps = { type EmailThreadMessageProps = {
body: string; body: string;
sentAt: string; sentAt: string;
sender: EmailThreadMessageParticipant;
participants: EmailThreadMessageParticipant[]; participants: EmailThreadMessageParticipant[];
isExpanded?: boolean; isExpanded?: boolean;
}; };
@ -37,17 +38,17 @@ type EmailThreadMessageProps = {
export const EmailThreadMessage = ({ export const EmailThreadMessage = ({
body, body,
sentAt, sentAt,
sender,
participants, participants,
isExpanded = false, isExpanded = false,
}: EmailThreadMessageProps) => { }: EmailThreadMessageProps) => {
const [isOpen, setIsOpen] = useState(isExpanded); const [isOpen, setIsOpen] = useState(isExpanded);
const from = participants.find((participant) => participant.role === 'from');
const receivers = participants.filter( const receivers = participants.filter(
(participant) => participant.role !== 'from', (participant) => participant.role !== 'from',
); );
if (!from || receivers.length === 0) { if (!sender || receivers.length === 0) {
return null; return null;
} }
@ -57,7 +58,7 @@ export const EmailThreadMessage = ({
style={{ cursor: isOpen ? 'auto' : 'pointer' }} style={{ cursor: isOpen ? 'auto' : 'pointer' }}
> >
<StyledThreadMessageHeader onClick={() => isOpen && setIsOpen(false)}> <StyledThreadMessageHeader onClick={() => isOpen && setIsOpen(false)}>
<EmailThreadMessageSender sender={from} sentAt={sentAt} /> <EmailThreadMessageSender sender={sender} sentAt={sentAt} />
{isOpen && <EmailThreadMessageReceivers receivers={receivers} />} {isOpen && <EmailThreadMessageReceivers receivers={receivers} />}
</StyledThreadMessageHeader> </StyledThreadMessageHeader>
<StyledThreadMessageBody> <StyledThreadMessageBody>

View File

@ -47,11 +47,7 @@ export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperation
id: true, id: true,
role: true, role: true,
displayName: true, displayName: true,
participant: { handle: true,
id: true,
email: true,
name: true,
},
person: true, person: true,
workspaceMember: true, workspaceMember: true,
}, },

View File

@ -1,9 +1,9 @@
import { useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useState } from 'react';
import { IconArrowsVertical } from 'twenty-ui'; import { IconArrowsVertical } from 'twenty-ui';
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage'; import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
import { EmailThreadMessage as EmailThreadMessageType } from '@/activities/emails/types/EmailThreadMessage'; import { EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender';
import { Button } from '@/ui/input/button/components/Button'; import { Button } from '@/ui/input/button/components/Button';
const StyledButtonContainer = styled.div` const StyledButtonContainer = styled.div`
@ -14,7 +14,7 @@ const StyledButtonContainer = styled.div`
export const IntermediaryMessages = ({ export const IntermediaryMessages = ({
messages, messages,
}: { }: {
messages: EmailThreadMessageType[]; messages: EmailThreadMessageWithSender[];
}) => { }) => {
const [areMessagesOpen, setAreMessagesOpen] = useState(false); const [areMessagesOpen, setAreMessagesOpen] = useState(false);
@ -26,6 +26,7 @@ export const IntermediaryMessages = ({
messages.map((message) => ( messages.map((message) => (
<EmailThreadMessage <EmailThreadMessage
key={message.id} key={message.id}
sender={message.sender}
participants={message.messageParticipants} participants={message.messageParticipants}
body={message.text} body={message.text}
sentAt={message.receivedAt} sentAt={message.receivedAt}

View File

@ -55,23 +55,11 @@ export const RightDrawerEmailThread = () => {
messageChannelLoading, messageChannelLoading,
} = useRightDrawerEmailThread(); } = 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(() => { useEffect(() => {
if (!visibleMessages[0]?.messageThread) { if (!messages[0]?.messageThread) {
return; return;
} }
setMessageThread(visibleMessages[0]?.messageThread); setMessageThread(messages[0]?.messageThread);
}); });
const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener( const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener(
@ -93,17 +81,17 @@ export const RightDrawerEmailThread = () => {
), ),
}); });
const visibleMessagesCount = visibleMessages.length; const messagesCount = messages.length;
const is5OrMoreMessages = visibleMessagesCount >= 5; const is5OrMoreMessages = messagesCount >= 5;
const firstMessages = visibleMessages.slice( const firstMessages = messages.slice(
0, 0,
is5OrMoreMessages ? 2 : visibleMessagesCount - 1, is5OrMoreMessages ? 2 : messagesCount - 1,
); );
const intermediaryMessages = is5OrMoreMessages const intermediaryMessages = is5OrMoreMessages
? visibleMessages.slice(2, visibleMessagesCount - 1) ? messages.slice(2, messagesCount - 1)
: []; : [];
const lastMessage = visibleMessages[visibleMessagesCount - 1]; const lastMessage = messages[messagesCount - 1];
const subject = visibleMessages[0]?.subject; const subject = messages[0]?.subject;
const canReply = useMemo(() => { const canReply = useMemo(() => {
return ( return (
@ -119,7 +107,7 @@ export const RightDrawerEmailThread = () => {
const url = `https://mail.google.com/mail/?authuser=${connectedAccountHandle}#all/${messageThreadExternalId}`; const url = `https://mail.google.com/mail/?authuser=${connectedAccountHandle}#all/${messageThreadExternalId}`;
window.open(url, '_blank'); window.open(url, '_blank');
}; };
if (!thread) { if (!thread || !messages.length) {
return null; return null;
} }
return ( return (
@ -136,6 +124,7 @@ export const RightDrawerEmailThread = () => {
{firstMessages.map((message) => ( {firstMessages.map((message) => (
<EmailThreadMessage <EmailThreadMessage
key={message.id} key={message.id}
sender={message.sender}
participants={message.messageParticipants} participants={message.messageParticipants}
body={message.text} body={message.text}
sentAt={message.receivedAt} sentAt={message.receivedAt}
@ -144,6 +133,7 @@ export const RightDrawerEmailThread = () => {
<IntermediaryMessages messages={intermediaryMessages} /> <IntermediaryMessages messages={intermediaryMessages} />
<EmailThreadMessage <EmailThreadMessage
key={lastMessage.id} key={lastMessage.id}
sender={lastMessage.sender}
participants={lastMessage.messageParticipants} participants={lastMessage.messageParticipants}
body={lastMessage.text} body={lastMessage.text}
sentAt={lastMessage.receivedAt} sentAt={lastMessage.receivedAt}

View File

@ -6,6 +6,8 @@ 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 { MessageChannel } from '@/accounts/types/MessageChannel';
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
import { EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender';
import { MessageChannelMessageAssociation } from '@/activities/emails/types/MessageChannelMessageAssociation'; 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';
@ -13,6 +15,7 @@ import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-ui';
export const useRightDrawerEmailThread = () => { export const useRightDrawerEmailThread = () => {
const viewableRecordId = useRecoilValue(viewableRecordIdState); const viewableRecordId = useRecoilValue(viewableRecordIdState);
@ -74,6 +77,30 @@ export const useRightDrawerEmailThread = () => {
} }
}, [messages, isMessagesFetchComplete]); }, [messages, isMessagesFetchComplete]);
// TODO: introduce nested filters so we can retrieve the message sender directly from the message query
const { records: messageSenders } =
useFindManyRecords<EmailThreadMessageParticipant>({
filter: {
messageId: {
in: messages.map(({ id }) => id),
},
role: {
eq: 'from',
},
},
objectNameSingular: CoreObjectNameSingular.MessageParticipant,
recordGqlFields: {
id: true,
role: true,
displayName: true,
messageId: true,
handle: true,
person: true,
workspaceMember: true,
},
skip: messages.length === 0,
});
const { records: messageChannelMessageAssociationData } = const { records: messageChannelMessageAssociationData } =
useFindManyRecords<MessageChannelMessageAssociation>({ useFindManyRecords<MessageChannelMessageAssociation>({
filter: { filter: {
@ -123,9 +150,24 @@ export const useRightDrawerEmailThread = () => {
const connectedAccountHandle = const connectedAccountHandle =
messageChannelData.length > 0 ? messageChannelData[0].handle : null; messageChannelData.length > 0 ? messageChannelData[0].handle : null;
const messagesWithSender: EmailThreadMessageWithSender[] = messages
.map((message) => {
const sender = messageSenders.find(
(messageSender) => messageSender.messageId === message.id,
);
if (!sender) {
return null;
}
return {
...message,
sender,
};
})
.filter(isDefined);
return { return {
thread, thread,
messages, messages: messagesWithSender,
messageThreadExternalId, messageThreadExternalId,
connectedAccountHandle, connectedAccountHandle,
threadLoading: messagesLoading, threadLoading: messagesLoading,

View File

@ -3,9 +3,12 @@ import { Person } from '@/people/types/Person';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
export type EmailThreadMessageParticipant = { export type EmailThreadMessageParticipant = {
id: string;
displayName: string; displayName: string;
handle: string; handle: string;
role: EmailParticipantRole; role: EmailParticipantRole;
messageId: string;
person: Person; person: Person;
workspaceMember: WorkspaceMember; workspaceMember: WorkspaceMember;
__typename: 'EmailThreadMessageParticipant';
}; };

View File

@ -0,0 +1,6 @@
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
export type EmailThreadMessageWithSender = EmailThreadMessage & {
sender: EmailThreadMessageParticipant;
};

View File

@ -4,9 +4,12 @@ import { getDisplayNameFromParticipant } from '../getDisplayNameFromParticipant'
describe('getDisplayNameFromParticipant', () => { describe('getDisplayNameFromParticipant', () => {
const participantWithName: EmailThreadMessageParticipant = { const participantWithName: EmailThreadMessageParticipant = {
id: '2cac0ba7-0e60-46c6-86e7-e5b0bc55b7cf',
__typename: 'EmailThreadMessageParticipant',
displayName: '', displayName: '',
handle: '', handle: '',
role: 'from', role: 'from',
messageId: '638f52d1-fd55-4a2b-b0f3-9858ea3b2e91',
person: { person: {
__typename: 'Person', __typename: 'Person',
id: '1', id: '1',