feat(messaging): sync draft emails and edit them in the thread composer (#22178)

Stop excluding drafts from sync across all three providers (Gmail DRAFT
label, Microsoft/IMAP Drafts folder) and add an isDraft boolean field on
Message so drafts are queryable by the API and AI agents.

Drafts render in the thread with a Draft tag; clicking one opens the
existing reply composer pre-filled with the draft's recipients, subject
and body, and Send reuses the existing send-email flow.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22178?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
neo773
2026-06-30 16:37:53 +05:30
committed by GitHub
parent c4a6446757
commit 9f3ebaaf22
60 changed files with 2057 additions and 1100 deletions
@@ -73,7 +73,7 @@ export const EmailComposerFields = ({
<StyledToRow>
<FormMultiTextFieldInput
label={t`To`}
defaultValue={composerState.defaultTo}
defaultValue={composerState.initialTo}
onChange={composerState.setTo}
placeholder={t`Recipients`}
/>
@@ -87,13 +87,13 @@ export const EmailComposerFields = ({
<>
<FormMultiTextFieldInput
label={t`Cc`}
defaultValue=""
defaultValue={composerState.initialCc}
onChange={composerState.setCc}
placeholder={t`Cc`}
/>
<FormMultiTextFieldInput
label={t`Bcc`}
defaultValue=""
defaultValue={composerState.initialBcc}
onChange={composerState.setBcc}
placeholder={t`Bcc`}
/>
@@ -101,12 +101,12 @@ export const EmailComposerFields = ({
)}
<FormTextFieldInput
label={t`Subject`}
defaultValue={composerState.defaultSubject}
defaultValue={composerState.initialSubject}
onChange={composerState.setSubject}
placeholder={t`Subject`}
/>
<FormAdvancedTextFieldInput
defaultValue=""
defaultValue={composerState.initialBody}
onChange={composerState.setBody}
placeholder={t`Type something or press "/" to see commands`}
minHeight={120}
@@ -1,92 +1,96 @@
import { styled } from '@linaria/react';
import { useState } from 'react';
import { EmailThreadMessageBody } from '@/activities/emails/components/EmailThreadMessageBody';
import { EmailThreadMessageBodyPreview } from '@/activities/emails/components/EmailThreadMessageBodyPreview';
import { EmailThreadMessageLayout } from '@/activities/emails/components/EmailThreadMessageLayout';
import { EmailThreadMessageReceivers } from '@/activities/emails/components/EmailThreadMessageReceivers';
import { EmailThreadMessageSender } from '@/activities/emails/components/EmailThreadMessageSender';
import { EmailThreadNotShared } from '@/activities/emails/components/EmailThreadNotShared';
import { type EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
import { type EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { MessageParticipantRole } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { MessageChannelVisibility } from '~/generated/graphql';
const StyledThreadMessage = styled.div<{ hideBottomBorder?: boolean }>`
border-bottom: ${({ hideBottomBorder }) =>
hideBottomBorder
? 'none'
: `1px solid ${themeCssVariables.border.color.light}`};
display: flex;
flex-direction: column;
padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[0]};
`;
const StyledThreadMessageHeader = styled.div`
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-between;
margin-bottom: ${themeCssVariables.spacing[2]};
padding: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[2]};
`;
const StyledThreadMessageBody = styled.div`
padding: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[2]};
`;
type EmailThreadMessageProps = {
body: string;
sentAt: string;
sender: EmailThreadMessageParticipant;
participants: EmailThreadMessageParticipant[];
message: EmailThreadMessageWithSender;
isExpanded?: boolean;
hideBottomBorder?: boolean;
onDraftClick: (message: EmailThreadMessageWithSender) => void;
};
export const EmailThreadMessage = ({
body,
sentAt,
sender,
participants,
message,
isExpanded = false,
hideBottomBorder = false,
onDraftClick,
}: EmailThreadMessageProps) => {
const [isOpen, setIsOpen] = useState(isExpanded);
const receivers = participants.filter(
const receivers = message.messageParticipants.filter(
(participant) => participant.role !== MessageParticipantRole.FROM,
);
if (!isDefined(sender) || receivers.length === 0) {
if (
!isDefined(message.sender) ||
(!message.isDraft && receivers.length === 0)
) {
return null;
}
const { isDraft } = message;
const isRestricted =
body === FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED;
message.text === FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED;
const handleRowClick = () => {
if (isRestricted) {
return;
}
if (isDraft) {
onDraftClick(message);
return;
}
if (!isOpen) {
setIsOpen(true);
}
};
const handleHeaderClick = () => {
if (!isDraft && isOpen) {
setIsOpen(false);
}
};
return (
<StyledThreadMessage
<EmailThreadMessageLayout
hideBottomBorder={hideBottomBorder}
onClick={() => !isOpen && setIsOpen(true)}
style={{ cursor: isOpen || isRestricted ? 'auto' : 'pointer' }}
>
<StyledThreadMessageHeader onClick={() => isOpen && setIsOpen(false)}>
<EmailThreadMessageSender sender={sender} sentAt={sentAt} />
{isOpen && <EmailThreadMessageReceivers receivers={receivers} />}
</StyledThreadMessageHeader>
<StyledThreadMessageBody>
{isRestricted ? (
<EmailThreadNotShared
visibility={MessageChannelVisibility.METADATA}
isRowClickable={!isRestricted && (isDraft || !isOpen)}
isHeaderClickable={!isDraft && isOpen}
onRowClick={handleRowClick}
onHeaderClick={handleHeaderClick}
header={
<>
<EmailThreadMessageSender
sender={message.sender}
sentAt={message.receivedAt}
/>
) : isOpen ? (
<EmailThreadMessageBody body={body} isDisplayed />
) : (
<EmailThreadMessageBodyPreview body={body} />
)}
</StyledThreadMessageBody>
</StyledThreadMessage>
{!isDraft && isOpen && (
<EmailThreadMessageReceivers receivers={receivers} />
)}
</>
}
>
{isRestricted ? (
<EmailThreadNotShared visibility={MessageChannelVisibility.METADATA} />
) : isDraft || !isOpen ? (
<EmailThreadMessageBodyPreview body={message.text} />
) : (
<EmailThreadMessageBody body={message.text} isDisplayed />
)}
</EmailThreadMessageLayout>
);
};
@@ -0,0 +1,57 @@
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledThreadMessage = styled.div<{ hideBottomBorder?: boolean }>`
border-bottom: ${({ hideBottomBorder }) =>
hideBottomBorder
? 'none'
: `1px solid ${themeCssVariables.border.color.light}`};
display: flex;
flex-direction: column;
padding: ${themeCssVariables.spacing[4]} ${themeCssVariables.spacing[0]};
`;
const StyledHeader = styled.div<{ isClickable?: boolean }>`
cursor: ${({ isClickable }) => (isClickable ? 'pointer' : 'auto')};
display: flex;
flex-direction: column;
justify-content: space-between;
margin-bottom: ${themeCssVariables.spacing[2]};
padding: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[2]};
`;
const StyledBody = styled.div`
padding: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[2]};
`;
type EmailThreadMessageLayoutProps = {
header: ReactNode;
children: ReactNode;
hideBottomBorder?: boolean;
isRowClickable?: boolean;
isHeaderClickable?: boolean;
onRowClick?: () => void;
onHeaderClick?: () => void;
};
export const EmailThreadMessageLayout = ({
header,
children,
hideBottomBorder = false,
isRowClickable = false,
isHeaderClickable = false,
onRowClick,
onHeaderClick,
}: EmailThreadMessageLayoutProps) => (
<StyledThreadMessage
hideBottomBorder={hideBottomBorder}
onClick={onRowClick}
style={{ cursor: isRowClickable ? 'pointer' : 'auto' }}
>
<StyledHeader isClickable={isHeaderClickable} onClick={onHeaderClick}>
{header}
</StyledHeader>
<StyledBody>{children}</StyledBody>
</StyledThreadMessage>
);
@@ -5,9 +5,10 @@ import { EmailThreadNotShared } from '@/activities/emails/components/EmailThread
import { useOpenRecordInSidePanel } from '@/side-panel/hooks/useOpenRecordInSidePanel';
import { useContext } from 'react';
import { t } from '@lingui/core/macro';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Avatar } from 'twenty-ui/data-display';
import { Avatar, Tag } from 'twenty-ui/data-display';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import {
MessageChannelVisibility,
@@ -173,6 +174,9 @@ export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
)}
{visibility === MessageChannelVisibility.SHARE_EVERYTHING && (
<>
{thread.lastMessageIsDraft && (
<Tag color="orange" text={t`Draft`} />
)}
<StyledSubject>{thread.subject}</StyledSubject>
<StyledBody>{thread.lastMessageBody}</StyledBody>
</>
@@ -5,6 +5,7 @@ export const SEND_EMAIL = gql`
sendEmail(input: $input) {
success
error
messageThreadId
}
}
`;
@@ -31,6 +31,7 @@ export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperation
subject: true,
text: true,
receivedAt: true,
isDraft: true,
messageThread: {
id: true,
},
@@ -18,6 +18,7 @@ export const timelineThreadFragment = gql`
subject
numberOfMessagesInThread
participantCount
lastMessageIsDraft
}
${participantFragment}
`;
@@ -3,13 +3,15 @@ import { MAX_EMAIL_RECIPIENTS } from 'twenty-shared/constants';
import { type EmailAttachment } from 'twenty-shared/types';
import { useSendEmail } from '@/activities/emails/hooks/useSendEmail';
import { type EmailDraftPrefill } from '@/activities/emails/types/EmailDraftPrefill';
type UseEmailComposerStateArgs = {
connectedAccountId: string;
draftPrefill?: EmailDraftPrefill | null;
defaultTo?: string;
defaultSubject?: string;
defaultInReplyTo?: string;
onSent?: () => void;
onSent?: (messageThreadId: string | null) => void;
};
const countRecipients = (csv: string): number =>
@@ -20,20 +22,29 @@ const countRecipients = (csv: string): number =>
export const useEmailComposerState = ({
connectedAccountId: initialConnectedAccountId,
draftPrefill,
defaultTo = '',
defaultSubject = '',
defaultInReplyTo,
onSent,
}: UseEmailComposerStateArgs) => {
const initialTo = draftPrefill?.to ?? defaultTo;
const initialCc = draftPrefill?.cc ?? '';
const initialBcc = draftPrefill?.bcc ?? '';
const initialSubject = draftPrefill?.subject ?? defaultSubject;
const initialBody = draftPrefill?.body ?? '';
const [connectedAccountId, setConnectedAccountId] = useState(
initialConnectedAccountId,
);
const [to, setTo] = useState(defaultTo);
const [cc, setCc] = useState('');
const [bcc, setBcc] = useState('');
const [subject, setSubject] = useState(defaultSubject);
const [body, setBody] = useState('');
const [showCcBcc, setShowCcBcc] = useState(false);
const [to, setTo] = useState(initialTo);
const [cc, setCc] = useState(initialCc);
const [bcc, setBcc] = useState(initialBcc);
const [subject, setSubject] = useState(initialSubject);
const [body, setBody] = useState(initialBody);
const [showCcBcc, setShowCcBcc] = useState(
initialCc.length > 0 || initialBcc.length > 0,
);
const [files, setFiles] = useState<EmailAttachment[]>([]);
const { sendEmail, loading } = useSendEmail();
@@ -60,7 +71,7 @@ export const useEmailComposerState = ({
const trimmedCc = cc.trim();
const trimmedBcc = bcc.trim();
const success = await sendEmail({
const { success, messageThreadId } = await sendEmail({
connectedAccountId,
to: trimmedTo,
cc: trimmedCc || undefined,
@@ -68,11 +79,12 @@ export const useEmailComposerState = ({
subject,
body,
inReplyTo: defaultInReplyTo,
draftMessageId: draftPrefill?.messageId,
files: files.length > 0 ? files : undefined,
});
if (success) {
onSent?.();
onSent?.(messageThreadId);
}
}, [
connectedAccountId,
@@ -82,6 +94,7 @@ export const useEmailComposerState = ({
subject,
body,
defaultInReplyTo,
draftPrefill?.messageId,
files,
sendEmail,
onSent,
@@ -108,8 +121,11 @@ export const useEmailComposerState = ({
handleSend,
loading,
canSend,
defaultTo,
defaultSubject,
initialTo,
initialCc,
initialBcc,
initialSubject,
initialBody,
recipientCount,
exceedsRecipientLimit,
maxRecipients: MAX_EMAIL_RECIPIENTS,
@@ -32,15 +32,27 @@ export const useReplyContext = (
return null;
}
const lastMessage = messages[messages.length - 1];
const sentMessages = messages.filter((message) => !message.isDraft);
const lastSentMessage = sentMessages[sentMessages.length - 1];
if (!isDefined(lastMessage)) {
return null;
if (!isDefined(lastSentMessage)) {
if (messages.length === 0) {
return null;
}
return {
loading: false,
to: '',
subject: '',
inReplyTo: '',
connectedAccountId,
connectedAccountProvider,
};
}
const senderHandle = lastMessage.sender?.handle ?? '';
const senderHandle = lastSentMessage.sender?.handle ?? '';
const rawSubject = lastMessage.subject ?? '';
const rawSubject = lastSentMessage.subject ?? '';
const subject = rawSubject.startsWith('Re: ')
? rawSubject
: `Re: ${rawSubject}`;
@@ -49,7 +61,7 @@ export const useReplyContext = (
loading: false,
to: senderHandle,
subject,
inReplyTo: lastMessage.headerMessageId ?? '',
inReplyTo: lastSentMessage.headerMessageId ?? '',
connectedAccountId,
connectedAccountProvider,
};
@@ -12,6 +12,11 @@ import {
type SendEmailMutationVariables,
} from '~/generated-metadata/graphql';
type SendEmailResult = {
success: boolean;
messageThreadId: string | null;
};
type SendEmailParams = {
connectedAccountId: string;
to: string;
@@ -20,6 +25,7 @@ type SendEmailParams = {
subject: string;
body: string;
inReplyTo?: string;
draftMessageId?: string;
files?: EmailAttachment[];
};
@@ -34,7 +40,7 @@ export const useSendEmail = () => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const sendEmail = useCallback(
async (params: SendEmailParams): Promise<boolean> => {
async (params: SendEmailParams): Promise<SendEmailResult> => {
try {
const result = await sendEmailMutation({
variables: {
@@ -46,6 +52,7 @@ export const useSendEmail = () => {
subject: params.subject,
body: params.body,
inReplyTo: params.inReplyTo,
draftMessageId: params.draftMessageId,
files: params.files,
},
},
@@ -65,20 +72,23 @@ export const useSendEmail = () => {
],
});
return true;
return {
success: true,
messageThreadId: result.data.sendEmail.messageThreadId ?? null,
};
}
enqueueErrorSnackBar({
message: result.data?.sendEmail.error ?? t`Failed to send email`,
});
return false;
return { success: false, messageThreadId: null };
} catch {
enqueueErrorSnackBar({
message: t`Failed to send email`,
});
return false;
return { success: false, messageThreadId: null };
}
},
[
@@ -0,0 +1,7 @@
import { type EmailRecipients } from 'twenty-shared/workflow';
export type EmailDraftPrefill = EmailRecipients & {
messageId: string;
subject: string;
body: string;
};
@@ -10,5 +10,6 @@ export type EmailThreadMessage = {
messageThreadId: string;
messageParticipants: EmailThreadMessageParticipant[];
messageThread: MessageThread;
isDraft: boolean;
__typename: 'EmailThreadMessage';
};
@@ -0,0 +1,22 @@
import { type EmailDraftPrefill } from '@/activities/emails/types/EmailDraftPrefill';
import { type EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender';
import { MessageParticipantRole } from 'twenty-shared/types';
export const getEmailDraftPrefillFromMessage = (
message: EmailThreadMessageWithSender,
): EmailDraftPrefill => {
const joinHandlesByRole = (role: MessageParticipantRole) =>
message.messageParticipants
.filter((participant) => participant.role === role)
.map((participant) => participant.handle)
.join(', ');
return {
messageId: message.id,
to: joinHandlesByRole(MessageParticipantRole.TO),
cc: joinHandlesByRole(MessageParticipantRole.CC),
bcc: joinHandlesByRole(MessageParticipantRole.BCC),
subject: message.subject,
body: message.text,
};
};
@@ -4,11 +4,15 @@ import { useCallback, useMemo } from 'react';
import { EmailComposerFields } from '@/activities/emails/components/EmailComposerFields';
import { useEmailComposerState } from '@/activities/emails/hooks/useEmailComposerState';
import { type ReplyContextReady } from '@/activities/emails/hooks/useReplyContext';
import { type EmailDraftPrefill } from '@/activities/emails/types/EmailDraftPrefill';
import { EmailThreadComposerFooterEffect } from '@/page-layout/widgets/email-thread/components/EmailThreadComposerFooterEffect';
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
import { useOpenRecordInSidePanel } from '@/side-panel/hooks/useOpenRecordInSidePanel';
import { type SidePanelFooterCommandMenuItem } from '@/ui/layout/side-panel/types/SidePanelFooterCommandMenuItem';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { t } from '@lingui/core/macro';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { IconArrowBackUp, IconSend, IconX } from 'twenty-ui/icon';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { getOsControlSymbol } from 'twenty-ui/utilities';
@@ -34,6 +38,7 @@ type EmailThreadComposerProps = {
isInSidePanel: boolean;
isComposerOpen: boolean;
setIsComposerOpen: (open: boolean) => void;
draftPrefill?: EmailDraftPrefill | null;
};
export const EmailThreadComposer = ({
@@ -41,13 +46,27 @@ export const EmailThreadComposer = ({
isInSidePanel,
isComposerOpen,
setIsComposerOpen,
draftPrefill,
}: EmailThreadComposerProps) => {
const handleReplySent = useCallback(() => {
setIsComposerOpen(false);
}, [setIsComposerOpen]);
const { openRecordInSidePanel } = useOpenRecordInSidePanel();
const handleReplySent = useCallback(
(messageThreadId: string | null) => {
setIsComposerOpen(false);
if (isDefined(messageThreadId)) {
openRecordInSidePanel({
recordId: messageThreadId,
objectNameSingular: CoreObjectNameSingular.MessageThread,
});
}
},
[setIsComposerOpen, openRecordInSidePanel],
);
const composerState = useEmailComposerState({
connectedAccountId: replyContext.connectedAccountId,
draftPrefill,
defaultTo: replyContext.to,
defaultSubject: replyContext.subject,
defaultInReplyTo: replyContext.inReplyTo,
@@ -15,8 +15,10 @@ const StyledButtonContainer = styled.div`
export const EmailThreadIntermediaryMessages = ({
messages,
onDraftClick,
}: {
messages: EmailThreadMessageWithSender[];
onDraftClick: (message: EmailThreadMessageWithSender) => void;
}) => {
const [areMessagesOpen, setAreMessagesOpen] = useState(false);
const messagesLength = messages.length;
@@ -29,10 +31,8 @@ export const EmailThreadIntermediaryMessages = ({
messages.map((message) => (
<EmailThreadMessage
key={message.id}
sender={message.sender}
participants={message.messageParticipants}
body={message.text}
sentAt={message.receivedAt}
message={message}
onDraftClick={onDraftClick}
/>
))
) : (
@@ -1,11 +1,14 @@
import { styled } from '@linaria/react';
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
import { EmailLoader } from '@/activities/emails/components/EmailLoader';
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
import { useEmailThread } from '@/activities/emails/hooks/useEmailThread';
import { useReplyContext } from '@/activities/emails/hooks/useReplyContext';
import { type EmailDraftPrefill } from '@/activities/emails/types/EmailDraftPrefill';
import { type EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender';
import { getEmailDraftPrefillFromMessage } from '@/activities/emails/utils/getEmailDraftPrefillFromMessage';
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
import { EmailThreadComposer } from '@/page-layout/widgets/email-thread/components/EmailThreadComposer';
import { EmailThreadIntermediaryMessages } from '@/page-layout/widgets/email-thread/components/EmailThreadIntermediaryMessages';
@@ -43,7 +46,36 @@ export const EmailThreadWidget = ({
const replyContext = useReplyContext(targetRecord.id);
const [isComposerOpen, setIsComposerOpen] = useState(false);
const [composerIntent, setComposerIntent] = useState<
'opened' | 'closed' | null
>(null);
const [clickedDraftPrefill, setClickedDraftPrefill] =
useState<EmailDraftPrefill | null>(null);
const [previousTargetRecordId, setPreviousTargetRecordId] = useState(
targetRecord.id,
);
if (previousTargetRecordId !== targetRecord.id) {
setPreviousTargetRecordId(targetRecord.id);
setComposerIntent(null);
setClickedDraftPrefill(null);
}
const handleComposerOpenChange = useCallback((open: boolean) => {
setComposerIntent(open ? 'opened' : 'closed');
if (!open) {
setClickedDraftPrefill(null);
}
}, []);
const handleDraftClick = useCallback(
(message: EmailThreadMessageWithSender) => {
setClickedDraftPrefill(getEmailDraftPrefillFromMessage(message));
setComposerIntent('opened');
},
[],
);
const canReply = isDefined(replyContext) && !replyContext.loading;
@@ -58,6 +90,16 @@ export const EmailThreadWidget = ({
: [];
const lastMessage = messages[messagesCount - 1];
const trailingDraft = lastMessage?.isDraft ? lastMessage : undefined;
const draftPrefill =
clickedDraftPrefill ??
(isDefined(trailingDraft)
? getEmailDraftPrefillFromMessage(trailingDraft)
: null);
const isComposerOpen =
composerIntent === 'opened' ||
(composerIntent === null && isDefined(trailingDraft));
if (threadLoading || !thread || !messages.length) {
return (
<StyledWrapper>
@@ -74,21 +116,20 @@ export const EmailThreadWidget = ({
{firstMessages.map((message) => (
<EmailThreadMessage
key={message.id}
sender={message.sender}
participants={message.messageParticipants}
body={message.text}
sentAt={message.receivedAt}
message={message}
onDraftClick={handleDraftClick}
/>
))}
<EmailThreadIntermediaryMessages messages={intermediaryMessages} />
<EmailThreadIntermediaryMessages
messages={intermediaryMessages}
onDraftClick={handleDraftClick}
/>
<EmailThreadMessage
key={lastMessage.id}
sender={lastMessage.sender}
participants={lastMessage.messageParticipants}
body={lastMessage.text}
sentAt={lastMessage.receivedAt}
message={lastMessage}
isExpanded
hideBottomBorder={!isComposerOpen}
onDraftClick={handleDraftClick}
/>
<CustomResolverFetchMoreLoader
loading={threadLoading}
@@ -97,10 +138,12 @@ export const EmailThreadWidget = ({
</StyledContainer>
{canReply && (
<EmailThreadComposer
key={draftPrefill?.messageId ?? 'reply'}
replyContext={replyContext}
isInSidePanel={isInSidePanel}
isComposerOpen={isComposerOpen}
setIsComposerOpen={setIsComposerOpen}
setIsComposerOpen={handleComposerOpenChange}
draftPrefill={draftPrefill}
/>
)}
</StyledWrapper>
@@ -1,6 +1,5 @@
import { getInitialEditorContent } from '@/workflow/workflow-variables/utils/getInitialEditorContent';
import type { JSONContent } from '@tiptap/react';
import { logError } from '~/utils/logError';
// Previous format of the email body was plain text,
// but from now on we will save it as JSON.
@@ -33,8 +32,7 @@ export const getInitialAdvancedTextEditorContent = (
}
return json;
} catch (error) {
logError(error);
} catch {
return getInitialEditorContent(rawContent);
}
};