Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 42 additions & 39 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"dependencies": {
"@internxt/css-config": "^1.1.0",
"@internxt/lib": "^1.4.1",
"@internxt/sdk": "^1.16.3",
"@internxt/sdk": "1.17.2",
"@internxt/ui": "^0.1.16",
"@phosphor-icons/react": "^2.1.10",
"@reduxjs/toolkit": "^2.11.2",
Expand All @@ -44,7 +44,7 @@
"dompurify": "^3.3.3",
"i18next": "^25.8.13",
"idb": "^8.0.3",
"internxt-crypto": "^1.3.0",
"internxt-crypto": "1.4.0",
"prettysize": "^2.0.0",
"react": "^19.2.0",
"react-device-detect": "^2.2.3",
Expand Down
145 changes: 130 additions & 15 deletions src/components/compose-message/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PaperclipIcon, XIcon } from '@phosphor-icons/react';
import { useCallback } from 'react';
import { LockKeyIcon, PaperclipIcon, WarningIcon, XIcon } from '@phosphor-icons/react';
import { useCallback, useMemo } from 'react';
import type { Recipient } from './types';
import { RecipientInput } from './components/RecipientInput';
import { Button, Input } from '@internxt/ui';
Expand All @@ -10,6 +10,16 @@
import useComposeMessage from './hooks/useComposeMessage';
import { useEditor } from '@tiptap/react';
import { EDITOR_CONFIG } from './config';
import {
useGetActiveDomainsQuery,
useGetMailAccountKeysQuery,
useLazyLookupRecipientKeysQuery,
useSendEmailMutation,
} from '@/store/api/mail';
import { classifyRecipients, uniqueEmailAddresses } from '@/utils/domain';
import { buildEncryptionBlock, type RecipientPublicKey } from '@/services/mail-encryption';
import notificationsService, { ToastType } from '@/services/notifications';
import type { EmailAddress, SendEmailRequest } from '@internxt/sdk/dist/mail/types';

export interface DraftMessage {
subject?: string;
Expand All @@ -19,6 +29,8 @@
body?: string;
}

const toEmailAddress = (r: Recipient): EmailAddress => (r.name ? { name: r.name, email: r.email } : { email: r.email });

export const ComposeMessageDialog = () => {
const { translate } = useTranslationContext();
const { closeDialog: onComposeMessageDialogClose, getDialogData: getComposeMessageDialogData } = useActionDialog();
Expand All @@ -45,15 +57,100 @@
const title = draft.subject ?? translate('modals.composeMessageDialog.title');
const editor = useEditor(EDITOR_CONFIG);

const { data: activeDomains } = useGetActiveDomainsQuery();
const { data: senderKeys } = useGetMailAccountKeysQuery();
const [triggerLookup] = useLazyLookupRecipientKeysQuery();
const [sendEmail, { isLoading: isSending }] = useSendEmailMutation();

const allRecipients = useMemo(
() => [...toRecipients, ...ccRecipients, ...bccRecipients],
[toRecipients, ccRecipients, bccRecipients],
);

const encryptionState = useMemo<'none' | 'encrypted' | 'cleartext'>(() => {
if (allRecipients.length === 0) return 'none';
if (!activeDomains) return 'none';
return classifyRecipients(
allRecipients.map((r) => r.email),
activeDomains,
).allInternxt
? 'encrypted'
: 'cleartext';
}, [allRecipients, activeDomains]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const onClose = useCallback(() => {
onComposeMessageDialogClose(ActionDialog.ComposeMessage);
}, [onComposeMessageDialogClose]);

const handlePrimaryAction = useCallback(() => {
const html = editor?.getHTML();
console.log('html', html);
onClose();
}, [editor, onClose]);
const handlePrimaryAction = useCallback(async () => {

Check failure on line 85 in src/components/compose-message/index.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=internxt_mail-web&issues=AZ5vT6uDXJbWEq1kPQnh&open=AZ5vT6uDXJbWEq1kPQnh&pullRequest=54
if (allRecipients.length === 0) {
notificationsService.show({
text: translate('modals.composeMessageDialog.errors.noRecipients'),
type: ToastType.Warning,
});
return;
}

const htmlBody = editor?.getHTML() ?? '';
const textBody = editor?.getText() ?? '';
const cleartextPayload: SendEmailRequest = {
to: toRecipients.map(toEmailAddress),
cc: ccRecipients.length ? ccRecipients.map(toEmailAddress) : undefined,
bcc: bccRecipients.length ? bccRecipients.map(toEmailAddress) : undefined,
subject: subjectValue,
textBody: textBody || undefined,
htmlBody: htmlBody || undefined,
};

try {
if (encryptionState === 'encrypted' && senderKeys?.address && senderKeys.publicKey) {
const uniqueAddresses = uniqueEmailAddresses(allRecipients.map((r) => r.email));
const lookup = await triggerLookup({ addresses: uniqueAddresses }).unwrap();
const usable = lookup.filter((r): r is { address: string; publicKey: string } => Boolean(r.publicKey));

if (usable.length === uniqueAddresses.length) {
const recipientsWithKeys: RecipientPublicKey[] = [
...usable,
{ address: senderKeys.address, publicKey: senderKeys.publicKey },
];
const encryption = await buildEncryptionBlock(
{ body: htmlBody || textBody, previewText: textBody },
recipientsWithKeys,
);
await sendEmail({
to: toRecipients.map(toEmailAddress),
cc: ccRecipients.length ? ccRecipients.map(toEmailAddress) : undefined,
bcc: bccRecipients.length ? bccRecipients.map(toEmailAddress) : undefined,
subject: subjectValue,
encryption,
}).unwrap();
} else {
await sendEmail(cleartextPayload).unwrap();
}
} else {
await sendEmail(cleartextPayload).unwrap();
}
onClose();
} catch {
notificationsService.show({
text: translate('modals.composeMessageDialog.errors.sendFailed'),
type: ToastType.Error,
});
}
}, [
allRecipients,
editor,
toRecipients,
ccRecipients,
bccRecipients,
subjectValue,
encryptionState,
senderKeys,
triggerLookup,
sendEmail,
onClose,
translate,
]);

if (!editor) return null;

Expand Down Expand Up @@ -100,15 +197,15 @@
showBccButton={!showBcc}
ccButtonText={translate('modals.composeMessageDialog.cc')}
bccButtonText={translate('modals.composeMessageDialog.bcc')}
disabled={false}
disabled={isSending}
/>
{showCc && (
<RecipientInput
label={translate('modals.composeMessageDialog.cc')}
recipients={ccRecipients}
onAddRecipient={(email) => onAddCcRecipient?.(email)}
onRemoveRecipient={(id) => onRemoveCcRecipient?.(id)}
disabled={false}
disabled={isSending}
/>
)}
{showBcc && (
Expand All @@ -117,28 +214,46 @@
recipients={bccRecipients}
onAddRecipient={(email) => onAddBccRecipient?.(email)}
onRemoveRecipient={(id) => onRemoveBccRecipient?.(id)}
disabled={false}
disabled={isSending}
/>
)}
<div className="flex flex-row gap-2 items-center">
<p className="font-medium max-w-16 w-full text-gray-100">
{translate('modals.composeMessageDialog.subject')}
</p>
<Input className="w-full" value={subjectValue} onChange={onSubjectChange} disabled={false} />
<Input className="w-full" value={subjectValue} onChange={onSubjectChange} disabled={isSending} />
</div>
<div className="w-full flex border border-gray-5" />
<EditorBar editor={editor} disabled={false} />
<EditorBar editor={editor} disabled={isSending} />
</div>
<div className="pt-4">
<RichTextEditor editor={editor} />
</div>
{/* !TODO: Handle attachments */}

<div className="mt-5 flex justify-end space-x-2">
<Button variant="ghost" onClick={() => {}} disabled={false}>
<div className="mt-5 flex justify-end items-center space-x-2">
{encryptionState === 'encrypted' && (
<span
data-testid="encryption-badge-encrypted"
className="inline-flex items-center gap-1 rounded-full bg-green/10 px-2.5 py-1 text-sm font-medium text-green"
>
<LockKeyIcon size={14} weight="fill" />
{translate('modals.composeMessageDialog.encryptedBadge')}
</span>
)}
{encryptionState === 'cleartext' && (
<span
data-testid="encryption-badge-cleartext"
className="inline-flex items-center gap-1 rounded-full bg-yellow/10 px-2.5 py-1 text-sm font-medium text-yellow"
>
<WarningIcon size={14} weight="fill" />
{translate('modals.composeMessageDialog.cleartextBadge')}
</span>
)}
<Button variant="ghost" onClick={() => {}} disabled={isSending}>
<PaperclipIcon size={24} />
</Button>
<Button onClick={handlePrimaryAction} loading={false} disabled={false} variant={'primary'}>
<Button onClick={handlePrimaryAction} loading={isSending} disabled={isSending} variant={'primary'}>
{translate('actions.send')}
</Button>
</div>
Expand Down
Loading
Loading