https://sonarly.com/issue/14482?type=bug Clicking "Reply" on an email thread in the side panel throws an unhandled "Account provider not supported" error for users with IMAP/SMTP connected accounts, crashing the UI. Fix: **Two bugs fixed in `SidePanelMessageThreadPage.tsx`:** **1. IMAP Reply crash (commit7c8d362772):** The IMAP Driver Integration PR added `IMAP_SMTP_CALDAV` to `ALLOWED_REPLY_PROVIDERS` and added `canReply` logic for IMAP accounts with SMTP params, but left `handleReplyClick` with a `throw new Error('Account provider not supported')` placeholder for the IMAP case. Unlike Google (Gmail URL) and Microsoft (Outlook URL), IMAP has no webmail URL to deep-link to for replies. **Fix:** Removed `IMAP_SMTP_CALDAV` from `ALLOWED_REPLY_PROVIDERS` so the Reply button is hidden for IMAP users. The switch statement keeps `IMAP_SMTP_CALDAV` as a no-op `break` to satisfy TypeScript exhaustiveness checking via `assertUnreachable`. Removed the now-unused `connectedAccountConnectionParameters` destructuring and IMAP-specific `canReply` condition. **2. `isDefined(canReply)` regression (commit9d57bc39e5):** The ESLint-to-OxLint migration mechanically replaced `canReply` truthiness checks with `isDefined(canReply)`. Since `canReply` is a boolean (from `useMemo`), `isDefined(false)` returns `true`, which meant: the Reply button was always rendered (line 169), never disabled (line 176), and the guard in `handleReplyClick` (line 106) never triggered. **Fix:** Reverted `isDefined(canReply)` back to `canReply` in the three affected locations: the render condition, the disabled prop, and the click handler guard.
177 lines
5.4 KiB
TypeScript
177 lines
5.4 KiB
TypeScript
import { styled } from '@linaria/react';
|
|
import { useEffect, useMemo } from 'react';
|
|
|
|
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
|
import { EmailLoader } from '@/activities/emails/components/EmailLoader';
|
|
import { EmailThreadHeader } from '@/activities/emails/components/EmailThreadHeader';
|
|
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
|
|
import { SidePanelMessageThreadIntermediaryMessages } from '@/side-panel/pages/message-thread/components/SidePanelMessageThreadIntermediaryMessages';
|
|
import { useEmailThreadInSidePanel } from '@/side-panel/pages/message-thread/hooks/useEmailThreadInSidePanel';
|
|
import { messageThreadComponentState } from '@/side-panel/pages/message-thread/states/messageThreadComponentState';
|
|
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
|
import { t } from '@lingui/core/macro';
|
|
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
|
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
|
import { IconArrowBackUp } from 'twenty-ui/display';
|
|
import { Button } from 'twenty-ui/input';
|
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
|
|
|
const StyledWrapper = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
`;
|
|
|
|
const StyledContainer = styled.div`
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
height: 85%;
|
|
overflow-y: auto;
|
|
`;
|
|
|
|
const StyledButtonContainer = styled.div`
|
|
background: ${themeCssVariables.background.secondary};
|
|
border-top: 1px solid ${themeCssVariables.border.color.light};
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
padding: ${themeCssVariables.spacing[2]};
|
|
width: 100%;
|
|
`;
|
|
|
|
const ALLOWED_REPLY_PROVIDERS = [
|
|
ConnectedAccountProvider.GOOGLE,
|
|
ConnectedAccountProvider.MICROSOFT,
|
|
];
|
|
|
|
export const SidePanelMessageThreadPage = () => {
|
|
const setMessageThread = useSetAtomComponentState(
|
|
messageThreadComponentState,
|
|
);
|
|
|
|
const {
|
|
thread,
|
|
messages,
|
|
fetchMoreMessages,
|
|
threadLoading,
|
|
messageThreadExternalId,
|
|
connectedAccountHandle,
|
|
messageChannelLoading,
|
|
connectedAccountProvider,
|
|
lastMessageExternalId,
|
|
} = useEmailThreadInSidePanel();
|
|
|
|
useEffect(() => {
|
|
if (!isDefined(messages[0]?.messageThread)) {
|
|
return;
|
|
}
|
|
setMessageThread(messages[0]?.messageThread);
|
|
}, [messages, setMessageThread]);
|
|
|
|
const messagesCount = messages.length;
|
|
const is5OrMoreMessages = messagesCount >= 5;
|
|
const firstMessages = messages.slice(
|
|
0,
|
|
is5OrMoreMessages ? 2 : messagesCount - 1,
|
|
);
|
|
const intermediaryMessages = is5OrMoreMessages
|
|
? messages.slice(2, messagesCount - 1)
|
|
: [];
|
|
const lastMessage = messages[messagesCount - 1];
|
|
const subject = messages[0]?.subject;
|
|
|
|
const canReply = useMemo(() => {
|
|
return (
|
|
connectedAccountHandle &&
|
|
connectedAccountProvider &&
|
|
ALLOWED_REPLY_PROVIDERS.includes(connectedAccountProvider) &&
|
|
isDefined(lastMessage) &&
|
|
messageThreadExternalId != null
|
|
);
|
|
}, [
|
|
connectedAccountHandle,
|
|
connectedAccountProvider,
|
|
lastMessage,
|
|
messageThreadExternalId,
|
|
]);
|
|
|
|
const handleReplyClick = () => {
|
|
if (!canReply) {
|
|
return;
|
|
}
|
|
|
|
let url: string;
|
|
switch (connectedAccountProvider) {
|
|
case ConnectedAccountProvider.MICROSOFT:
|
|
url = `https://outlook.office.com/mail/deeplink?ItemID=${lastMessageExternalId}`;
|
|
window.open(url, '_blank');
|
|
break;
|
|
case ConnectedAccountProvider.GOOGLE:
|
|
url = `https://mail.google.com/mail/?authuser=${connectedAccountHandle}#all/${messageThreadExternalId}`;
|
|
window.open(url, '_blank');
|
|
break;
|
|
case ConnectedAccountProvider.IMAP_SMTP_CALDAV:
|
|
case null:
|
|
break;
|
|
default:
|
|
assertUnreachable(connectedAccountProvider);
|
|
}
|
|
};
|
|
if (!thread || !messages.length) {
|
|
return null;
|
|
}
|
|
return (
|
|
<StyledWrapper>
|
|
<StyledContainer>
|
|
{threadLoading ? (
|
|
<EmailLoader loadingText={t`Loading thread`} />
|
|
) : (
|
|
<>
|
|
<EmailThreadHeader
|
|
subject={subject}
|
|
lastMessageSentAt={lastMessage.receivedAt}
|
|
/>
|
|
{firstMessages.map((message) => (
|
|
<EmailThreadMessage
|
|
key={message.id}
|
|
sender={message.sender}
|
|
participants={message.messageParticipants}
|
|
body={message.text}
|
|
sentAt={message.receivedAt}
|
|
/>
|
|
))}
|
|
<SidePanelMessageThreadIntermediaryMessages
|
|
messages={intermediaryMessages}
|
|
/>
|
|
<EmailThreadMessage
|
|
key={lastMessage.id}
|
|
sender={lastMessage.sender}
|
|
participants={lastMessage.messageParticipants}
|
|
body={lastMessage.text}
|
|
sentAt={lastMessage.receivedAt}
|
|
isExpanded
|
|
/>
|
|
<CustomResolverFetchMoreLoader
|
|
loading={threadLoading}
|
|
onLastRowVisible={fetchMoreMessages}
|
|
/>
|
|
</>
|
|
)}
|
|
</StyledContainer>
|
|
{canReply && !messageChannelLoading && (
|
|
<StyledButtonContainer>
|
|
<Button
|
|
size="small"
|
|
onClick={handleReplyClick}
|
|
title={t`Reply`}
|
|
Icon={IconArrowBackUp}
|
|
disabled={!canReply}
|
|
/>
|
|
</StyledButtonContainer>
|
|
)}
|
|
</StyledWrapper>
|
|
);
|
|
};
|