7242 error when displaying message threads with a large number of participants (#7251)
Closes #7242
This commit is contained in:
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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';
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
|
||||||
|
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
|
||||||
|
|
||||||
|
export type EmailThreadMessageWithSender = EmailThreadMessage & {
|
||||||
|
sender: EmailThreadMessageParticipant;
|
||||||
|
};
|
||||||
@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user