diff --git a/package-lock.json b/package-lock.json index 3849289..8fed374 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "dependencies": { "@internxt/css-config": "^1.1.0", "@internxt/lib": "^1.4.1", - "@internxt/sdk": "^1.17.10", - "@internxt/ui": "^0.1.16", + "@internxt/sdk": "^1.17.11", + "@internxt/ui": "^0.1.25", "@phosphor-icons/react": "^2.1.10", "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/vite": "^4.2.1", @@ -1455,18 +1455,18 @@ "license": "MIT" }, "node_modules/@internxt/sdk": { - "version": "1.17.10", - "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.17.10.tgz", - "integrity": "sha512-gxpXc70c9pYVcQepTDwyhkB6zJ8i7Ea87FJNArMYwIliRNJllsNjdwQ8DV/MqH6iVY5EqbOfNHP3ZfurCdCq/w==", + "version": "1.17.11", + "resolved": "https://registry.npmjs.org/@internxt/sdk/-/sdk-1.17.11.tgz", + "integrity": "sha512-t9rTaNNli2GdnFTEJPgcI2MAX2LouKzVbFIBj6QUVlCk7Ivcj+hbkkZqVlHDNTvsw3UObq2ehSNYWmzkMgcbfQ==", "license": "MIT", "dependencies": { "axios": "^1.16.0" } }, "node_modules/@internxt/ui": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@internxt/ui/-/ui-0.1.16.tgz", - "integrity": "sha512-V9VQrUVZehvx5L3FChkCfwmOq/wnQsFSSjcfGHMb70SVKZQAfCSv3ZyjcnL5iIaLInWGwtwspmtMfl3qh0zVWQ==", + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@internxt/ui/-/ui-0.1.25.tgz", + "integrity": "sha512-xtDHnzLA3ml4FBLuNY15wm8vPQ59J4qSzim7sb/bXKZpJqlpt+kHbDtpLzzB6P+/l1QthXVO4VGDH0m44rlpug==", "license": "MIT", "dependencies": { "@internxt/css-config": "1.1.0", diff --git a/package.json b/package.json index 79411df..54c7d41 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "dependencies": { "@internxt/css-config": "^1.1.0", "@internxt/lib": "^1.4.1", - "@internxt/sdk": "^1.17.10", - "@internxt/ui": "^0.1.16", + "@internxt/sdk": "^1.17.11", + "@internxt/ui": "^0.1.25", "@phosphor-icons/react": "^2.1.10", "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/vite": "^4.2.1", diff --git a/src/features/mail/MailView.tsx b/src/features/mail/MailView.tsx index 225d078..379300c 100644 --- a/src/features/mail/MailView.tsx +++ b/src/features/mail/MailView.tsx @@ -51,7 +51,7 @@ const MailView = ({ folder }: MailViewProps) => { toggleUnreadFilter, } = useListFolderPaginated(folder); - const { selectedEmails, selectAll, selectNone, selectRead, selectUnread, toggleSelectAll } = + const { selectedEmails, selectAll, selectEmail, selectNone, selectRead, selectUnread, toggleSelectAll } = useMailSelection(listFolderEmails); const { listActionContext, bulkActionContext } = useListActionContext(folder, selectedEmails, { selectAll, @@ -134,6 +134,7 @@ const MailView = ({ folder }: MailViewProps) => { onLoadMore={onLoadMore} emptyState={} onMailSelected={onSelectEmail} + onMailChecked={selectEmail} /> diff --git a/src/features/mail/components/tray/search/components/list/index.tsx b/src/features/mail/components/tray/search/components/list/index.tsx index 06bda4e..96f5ab5 100644 --- a/src/features/mail/components/tray/search/components/list/index.tsx +++ b/src/features/mail/components/tray/search/components/list/index.tsx @@ -42,7 +42,13 @@ const SearchEmailList = ({ mails, hasMoreItems, loading, onLoadMore, onMailSelec > {formattedMails.map((email) => (
- +
))} diff --git a/src/hooks/mail/useMailSelection.ts b/src/hooks/mail/useMailSelection.ts index 3891fe0..1d55f73 100644 --- a/src/hooks/mail/useMailSelection.ts +++ b/src/hooks/mail/useMailSelection.ts @@ -10,6 +10,14 @@ export const useMailSelection = (emails: EmailListResponse['emails'] | undefined const selectNone = () => setSelectedEmails([]); + const selectEmail = (emailId: string) => { + setSelectedEmails((prev) => { + if (prev.includes(emailId)) return prev.filter((id) => id !== emailId); + if (!emails?.some((email) => email.id === emailId)) return prev; + return [...prev, emailId]; + }); + }; + const selectRead = () => { setSelectedEmails(emails?.filter((email) => email.isRead).map((email) => email.id) ?? []); }; @@ -31,6 +39,7 @@ export const useMailSelection = (emails: EmailListResponse['emails'] | undefined return { selectedEmails, selectAll, + selectEmail, selectNone, selectRead, selectUnread, diff --git a/src/test-utils/fixtures.ts b/src/test-utils/fixtures.ts index 7425cb9..f2f59bf 100644 --- a/src/test-utils/fixtures.ts +++ b/src/test-utils/fixtures.ts @@ -169,6 +169,7 @@ export const getMockedMail = (overrides?: Partial): EmailResponse hasAttachment: faker.datatype.boolean(), size: faker.number.int({ min: 1024, max: 16384 }), attachments: faker.datatype.boolean() ? [getMockedAttachment()] : [], + isDraft: false, ...overrides, }); @@ -176,6 +177,7 @@ export const getMockedMails = (count = 3): EmailListResponse => ({ emails: Array.from({ length: count }, () => { const mail = getMockedMail(); return { + isDraft: false, id: mail.id, mailboxIds: mail.mailboxIds, threadId: mail.threadId, diff --git a/src/utils/format-emails/index.test.ts b/src/utils/format-emails/index.test.ts index 0a5df9c..567cb6b 100644 --- a/src/utils/format-emails/index.test.ts +++ b/src/utils/format-emails/index.test.ts @@ -28,11 +28,34 @@ describe('Formatting emails to list format', () => { expect(item.subject).toBe(emails[i].subject); expect(item.body).toBe(emails[i].preview); expect(item.read).toBe(emails[i].isRead); - expect(item.from.avatar).toBe(''); + expect(item.from).toEqual(expect.arrayContaining([expect.objectContaining({ avatar: '' })])); expect(DateService.formatMailTimestamp).toHaveBeenCalledWith(emails[0].receivedAt); }); }); + test('When the row carries thread participants, then those are used instead of the message sender', () => { + const { emails } = getMockedMails(1); + emails[0].from = [{ email: 'alice@inxt.me', name: 'Alice' }]; + (emails[0] as { participants?: { email: string; name?: string }[] }).participants = [ + { email: 'alice@inxt.me', name: 'Alice' }, + { email: 'bob@inxt.me', name: 'Bob' }, + ]; + + const result = formatEmailsToList(emails); + + expect(result?.[0].from.map((p) => p.name)).toEqual(['Alice', 'Bob']); + }); + + test('When the row has no participants, then the message sender is used as fallback', () => { + const { emails } = getMockedMails(1); + emails[0].from = [{ email: 'alice@inxt.me', name: 'Alice' }]; + delete (emails[0] as { participants?: unknown }).participants; + + const result = formatEmailsToList(emails); + + expect(result?.[0].from.map((p) => p.name)).toEqual(['Alice']); + }); + test('When a row is encrypted, then it uses the decrypted preview when available and never the ciphertext', () => { const { emails } = getMockedMails(2); emails.forEach((e) => { diff --git a/src/utils/format-emails/index.ts b/src/utils/format-emails/index.ts index f39c5a2..a6a8053 100644 --- a/src/utils/format-emails/index.ts +++ b/src/utils/format-emails/index.ts @@ -1,18 +1,22 @@ import { DateService } from '@/services/date'; import type { EmailListResponse } from '@internxt/sdk/dist/mail/types'; +type EmailSummary = EmailListResponse['emails'][number]; + +const toParticipant = (sender: EmailSummary['from'][number]) => ({ + name: sender.name ?? sender.email.split('@')[0], + avatar: '', +}); + export const formatEmailsToList = ( listFolderEmails?: EmailListResponse['emails'], decryptedPreviews?: Record, ) => { return listFolderEmails?.map((mail) => ({ id: mail.id, - from: { - name: mail.from[0]?.name ?? mail.from[0]?.email ?? '', - avatar: '', - }, + from: (mail.participants?.length ? mail.participants : mail.from).map(toParticipant), subject: mail.subject, - createdAt: DateService.formatMailTimestamp(mail.receivedAt), + createdAt: DateService.formatMailTimestamp(mail.lastReceivedAt ?? mail.receivedAt), body: decryptedPreviews?.[mail.id] ?? (mail.encryption ? '' : mail.preview), read: mail.isRead, }));