fix(emails): stop reply composer infinite re-render (#21935)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21935?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. -->
This commit is contained in:
neo773
2026-06-23 04:27:54 +05:30
committed by GitHub
parent a5aac3c21e
commit 242b989c0e
2 changed files with 78 additions and 60 deletions
@@ -1,14 +1,13 @@
import { styled } from '@linaria/react';
import { useCallback, useEffect, useMemo } from 'react';
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 { EmailThreadComposerFooterEffect } from '@/page-layout/widgets/email-thread/components/EmailThreadComposerFooterEffect';
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
import { sidePanelWidgetFooterCommandMenuItemsState } from '@/ui/layout/side-panel/states/sidePanelWidgetFooterCommandMenuItemsState';
import { type SidePanelFooterCommandMenuItem } from '@/ui/layout/side-panel/types/SidePanelFooterCommandMenuItem';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { t } from '@lingui/core/macro';
import { IconArrowBackUp, IconSend, IconX } from 'twenty-ui/icon';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -55,58 +54,47 @@ export const EmailThreadComposer = ({
onSent: handleReplySent,
});
const setSidePanelWidgetFooterCommandMenuItems = useSetAtomState(
sidePanelWidgetFooterCommandMenuItemsState,
);
const { handleSend, canSend } = composerState;
const footerCommandMenuItems =
useMemo((): SidePanelFooterCommandMenuItem[] => {
if (!isComposerOpen) {
return [
{
id: 'reply',
label: t`Reply`,
Icon: IconArrowBackUp,
isPrimaryCTA: true,
onClick: () => setIsComposerOpen(true),
},
];
}
const footerActions = useMemo((): SidePanelFooterCommandMenuItem[] => {
if (!isComposerOpen) {
return [
{
id: 'reply',
label: t`Reply`,
Icon: IconArrowBackUp,
id: 'cancel-reply',
label: t`Cancel reply`,
Icon: IconX,
isPinned: false,
onClick: () => setIsComposerOpen(false),
},
{
id: 'send',
label: t`Send`,
Icon: IconSend,
isPrimaryCTA: true,
onClick: () => setIsComposerOpen(true),
hotkeys: [getOsControlSymbol(), '⏎'],
onClick: handleSend,
disabled: !canSend,
},
];
}
return [
{
id: 'cancel-reply',
label: t`Cancel reply`,
Icon: IconX,
isPinned: false,
onClick: () => setIsComposerOpen(false),
},
{
id: 'send',
label: t`Send`,
Icon: IconSend,
isPrimaryCTA: true,
hotkeys: [getOsControlSymbol(), '⏎'],
onClick: composerState.handleSend,
disabled: !composerState.canSend,
},
];
}, [isComposerOpen, composerState, setIsComposerOpen]);
useEffect(() => {
if (!isInSidePanel) {
return;
}
setSidePanelWidgetFooterCommandMenuItems(footerActions);
return () => setSidePanelWidgetFooterCommandMenuItems([]);
}, [isInSidePanel, footerActions, setSidePanelWidgetFooterCommandMenuItems]);
}, [isComposerOpen, handleSend, canSend, setIsComposerOpen]);
const handleSendHotkey = useCallback(() => {
if (isComposerOpen && composerState.canSend) {
composerState.handleSend();
if (isComposerOpen && canSend) {
handleSend();
}
}, [isComposerOpen, composerState]);
}, [isComposerOpen, canSend, handleSend]);
useHotkeysOnFocusedElement({
keys: ['ctrl+Enter,meta+Enter'],
@@ -115,18 +103,23 @@ export const EmailThreadComposer = ({
dependencies: [handleSendHotkey],
});
if (!isComposerOpen) {
if (isInSidePanel) {
return null;
}
return (
<StyledReplyBar onClick={() => setIsComposerOpen(true)}>
<IconArrowBackUp size={16} />
{t`Reply...`}
</StyledReplyBar>
);
}
return <EmailComposerFields composerState={composerState} />;
return (
<>
{isInSidePanel && (
<EmailThreadComposerFooterEffect
footerCommandMenuItems={footerCommandMenuItems}
/>
)}
{isComposerOpen ? (
<EmailComposerFields composerState={composerState} />
) : (
!isInSidePanel && (
<StyledReplyBar onClick={() => setIsComposerOpen(true)}>
<IconArrowBackUp size={16} />
{t`Reply...`}
</StyledReplyBar>
)
)}
</>
);
};
@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import { sidePanelWidgetFooterCommandMenuItemsState } from '@/ui/layout/side-panel/states/sidePanelWidgetFooterCommandMenuItemsState';
import { type SidePanelFooterCommandMenuItem } from '@/ui/layout/side-panel/types/SidePanelFooterCommandMenuItem';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
type EmailThreadComposerFooterEffectProps = {
footerCommandMenuItems: SidePanelFooterCommandMenuItem[];
};
export const EmailThreadComposerFooterEffect = ({
footerCommandMenuItems,
}: EmailThreadComposerFooterEffectProps) => {
const setSidePanelWidgetFooterCommandMenuItems = useSetAtomState(
sidePanelWidgetFooterCommandMenuItemsState,
);
useEffect(() => {
setSidePanelWidgetFooterCommandMenuItems(footerCommandMenuItems);
return () => setSidePanelWidgetFooterCommandMenuItems([]);
}, [footerCommandMenuItems, setSidePanelWidgetFooterCommandMenuItems]);
return <></>;
};