@nocobase/app-plugin-mail 1.0.0-beta.11 → 1.0.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +10 -2
  3. package/dist/client/components/mail-account-connector.d.ts +1 -0
  4. package/dist/client/components/mail-account-connector.d.ts.map +1 -1
  5. package/dist/client/components/mail-account-connector.js +4 -3
  6. package/dist/client/components/mail-account-connector.js.map +1 -1
  7. package/dist/client/components/mail-navigation-icon.d.ts.map +1 -1
  8. package/dist/client/components/mail-navigation-icon.js +3 -50
  9. package/dist/client/components/mail-navigation-icon.js.map +1 -1
  10. package/dist/client/components/public.d.ts +1 -1
  11. package/dist/client/components/public.d.ts.map +1 -1
  12. package/dist/client/components/public.js.map +1 -1
  13. package/dist/client/contracts/composer.d.ts +29 -2
  14. package/dist/client/contracts/composer.d.ts.map +1 -1
  15. package/dist/client/contracts/composer.js.map +1 -1
  16. package/dist/client/hooks/use-mail-composer.d.ts +1 -1
  17. package/dist/client/hooks/use-mail-composer.d.ts.map +1 -1
  18. package/dist/client/hooks/use-mail-composer.js +54 -10
  19. package/dist/client/hooks/use-mail-composer.js.map +1 -1
  20. package/dist/client/hooks/use-mail-unread-count.d.ts +13 -0
  21. package/dist/client/hooks/use-mail-unread-count.d.ts.map +1 -0
  22. package/dist/client/hooks/use-mail-unread-count.js +106 -0
  23. package/dist/client/hooks/use-mail-unread-count.js.map +1 -0
  24. package/dist/client/index.d.ts +3 -0
  25. package/dist/client/index.d.ts.map +1 -1
  26. package/dist/client/index.js +2 -0
  27. package/dist/client/index.js.map +1 -1
  28. package/dist/client/lib/mail-composer-completion.d.ts +5 -0
  29. package/dist/client/lib/mail-composer-completion.d.ts.map +1 -0
  30. package/dist/client/lib/mail-composer-completion.js +28 -0
  31. package/dist/client/lib/mail-composer-completion.js.map +1 -0
  32. package/dist/client/locales/en-US.d.ts +1 -0
  33. package/dist/client/locales/en-US.d.ts.map +1 -1
  34. package/dist/client/locales/en-US.js +4 -3
  35. package/dist/client/locales/en-US.js.map +1 -1
  36. package/dist/client/locales/zh-CN.d.ts.map +1 -1
  37. package/dist/client/locales/zh-CN.js +3 -2
  38. package/dist/client/locales/zh-CN.js.map +1 -1
  39. package/dist/client/pages/mail-accounts-page.d.ts.map +1 -1
  40. package/dist/client/pages/mail-accounts-page.js +4 -1
  41. package/dist/client/pages/mail-accounts-page.js.map +1 -1
  42. package/dist/client/pages/mail-workspace-page.d.ts +4 -2
  43. package/dist/client/pages/mail-workspace-page.d.ts.map +1 -1
  44. package/dist/client/pages/mail-workspace-page.js +222 -171
  45. package/dist/client/pages/mail-workspace-page.js.map +1 -1
  46. package/dist/client/prepare-mail-reply.d.ts +9 -0
  47. package/dist/client/prepare-mail-reply.d.ts.map +1 -0
  48. package/dist/client/prepare-mail-reply.js +28 -0
  49. package/dist/client/prepare-mail-reply.js.map +1 -0
  50. package/dist/server/config.d.ts +1 -1
  51. package/dist/server/config.d.ts.map +1 -1
  52. package/dist/server/config.js +17 -9
  53. package/dist/server/config.js.map +1 -1
  54. package/dist/server/locales/en-US.d.ts +1 -0
  55. package/dist/server/locales/en-US.d.ts.map +1 -1
  56. package/dist/server/locales/en-US.js +1 -0
  57. package/dist/server/locales/en-US.js.map +1 -1
  58. package/dist/server/locales/zh-CN.d.ts.map +1 -1
  59. package/dist/server/locales/zh-CN.js +1 -0
  60. package/dist/server/locales/zh-CN.js.map +1 -1
  61. package/dist/server/providers/mail-core.d.ts +2 -0
  62. package/dist/server/providers/mail-core.d.ts.map +1 -1
  63. package/dist/server/providers/mail-core.js +39 -1
  64. package/dist/server/providers/mail-core.js.map +1 -1
  65. package/dist/server/routes/api.js +3 -1
  66. package/dist/server/routes/api.js.map +1 -1
  67. package/dist/server/services/authorization.js +1 -1
  68. package/dist/server/services/authorization.js.map +1 -1
  69. package/dist/shared/mail.d.ts +1 -1
  70. package/dist/shared/mail.d.ts.map +1 -1
  71. package/package.json +8 -5
  72. package/skills/nocobase-app-plugin-mail/references/client-integration.md +106 -0
  73. package/skills/nocobase-app-plugin-mail/references/configuration-and-accounts.md +10 -4
  74. package/skills/nocobase-app-plugin-mail/references/public-api.md +25 -20
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PageContainer } from '../components/page-container.js';
3
3
  import { PageHeader } from '../components/page-header.js';
4
- import { prepareReplyContent } from '../lib/mail-reply-content.js';
4
+ import { prepareMailReply } from '../prepare-mail-reply.js';
5
5
  import { mailErrorDescription } from '../lib/mail-error-description.js';
6
6
  import { useMailWorkspaceData } from '../hooks/use-mail-workspace-data.js';
7
7
  import { createForwardQuote, readDraftComposerBody, } from '../lib/mail-forward-content.js';
8
8
  import { EMPTY_COMPOSER, } from '../lib/mail-composer-state.js';
9
- import { ChevronLeft, ChevronRight, Inbox, PenLine, RefreshCw, Search, } from 'lucide-react';
9
+ import { ChevronLeft, ChevronRight, Mailbox, Inbox, PenLine, RefreshCw, Search, } from 'lucide-react';
10
10
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
11
11
  import { useTranslation } from '@nocobase/i18n/client';
12
12
  import { resolveAppUrl } from '@nocobase/app-client';
@@ -14,6 +14,8 @@ import { MailWorkspaceComposer } from '../components/mail-workspace-composer.js'
14
14
  import { MailboxSidebar, MailConversationView, MailMessageList, MAIL_UNREAD_COUNT_CHANGED_EVENT, } from '../components/index.js';
15
15
  import { Button } from '../components/ui/button.js';
16
16
  import { Input } from '../components/ui/input.js';
17
+ import { Sheet, SheetContent, SheetHeader, SheetTitle, } from '../components/ui/sheet.js';
18
+ import { cn } from '../lib/utils.js';
17
19
  import { getMailFolderDisplayName, getMailFolderSections, getVirtualMailFolders, } from '../lib/mail-folders.js';
18
20
  import { mailErrorMessage, } from '../mail-client.js';
19
21
  import { useMailClient } from '../runtime.js';
@@ -21,7 +23,7 @@ import { mailSyncDateMonthsAgo } from '../../shared/mail-sync-date.js';
21
23
  import { MAIL_VIRTUAL_FOLDER_IDS } from '../../shared/mail.js';
22
24
  import { MAIL_PLUGIN_NS } from '../namespace.js';
23
25
  const LAST_COMPOSE_ACCOUNT_KEY_PREFIX = 'nocobase:mail:last-compose-account:v1:';
24
- export default function MailWorkspacePage({ title, description, templateVariables = {}, accountsHref, } = {}) {
26
+ export default function MailWorkspacePage({ title, description, accountsHref, headerActions, templateVariables = {}, } = {}) {
25
27
  const { t } = useTranslation(MAIL_PLUGIN_NS);
26
28
  const mail = useMailClient();
27
29
  const [accounts, setAccounts] = useState([]);
@@ -46,6 +48,24 @@ export default function MailWorkspacePage({ title, description, templateVariable
46
48
  const [debouncedQuery, setDebouncedQuery] = useState('');
47
49
  const [composeAccountId, setComposeAccountId] = useState('');
48
50
  const [composerRequest, setComposerRequest] = useState();
51
+ const [navigationOpen, setNavigationOpen] = useState(false);
52
+ const workspaceRef = useRef(null);
53
+ const navigationButtonRef = useRef(null);
54
+ const conversationBackRef = useRef(null);
55
+ const selectedRowRef = useRef(null);
56
+ useEffect(() => {
57
+ if (!navigationOpen ||
58
+ !navigationButtonRef.current ||
59
+ typeof ResizeObserver === 'undefined')
60
+ return;
61
+ // CSS owns the breakpoint; observing its trigger also handles root font changes.
62
+ const observer = new ResizeObserver(() => {
63
+ if (!navigationButtonRef.current?.getClientRects().length)
64
+ setNavigationOpen(false);
65
+ });
66
+ observer.observe(navigationButtonRef.current);
67
+ return () => observer.disconnect();
68
+ }, [navigationOpen]);
49
69
  useEffect(() => {
50
70
  syncRunsRef.current = syncRuns;
51
71
  }, [syncRuns]);
@@ -70,6 +90,14 @@ export default function MailWorkspacePage({ title, description, templateVariable
70
90
  setError,
71
91
  onFocus: () => loadAccounts(false),
72
92
  });
93
+ const selectedMessageKey = selected
94
+ ? JSON.stringify([selected.accountId, selected.id])
95
+ : undefined;
96
+ useEffect(() => {
97
+ if (selectedMessageKey &&
98
+ navigationButtonRef.current?.getClientRects().length)
99
+ conversationBackRef.current?.focus({ preventScroll: true });
100
+ }, [selectedMessageKey]);
73
101
  const currentAccount = accounts.find((account) => account.id === accountId);
74
102
  const currentProviderCapabilities = findProviderCapabilities(currentAccount, providers);
75
103
  const composeAccount = accounts.find((account) => account.id === composeAccountId);
@@ -436,6 +464,11 @@ export default function MailWorkspacePage({ title, description, templateVariable
436
464
  const syncLabel = isAllAccounts
437
465
  ? t('workspace.syncAll', { defaultValue: 'Sync all mailboxes' })
438
466
  : t('workspace.incrementalRefresh', { defaultValue: 'Sync mailbox' });
467
+ const closeMailboxNavigation = () => {
468
+ setNavigationOpen(false);
469
+ if (navigationOpen)
470
+ clearSelection();
471
+ };
439
472
  const mailboxNavigation = (_jsx(MailboxSidebar, { accountId: accountId, accounts: accounts, customLabels: customLabels, folderId: folderId, folderAccountId: folderAccountId, folders: folders, accountFolderGroups: accountFolderGroups, labelId: labelId, labels: {
440
473
  account: t('dev.account', { defaultValue: 'Account' }),
441
474
  allAccounts: t('workspace.allAccounts', {
@@ -449,6 +482,7 @@ export default function MailWorkspacePage({ title, description, templateVariable
449
482
  folderNames,
450
483
  systemFolderNames,
451
484
  }, onAccountChange: (value) => {
485
+ closeMailboxNavigation();
452
486
  if (value === accountId)
453
487
  return;
454
488
  cancelRequests();
@@ -462,6 +496,7 @@ export default function MailWorkspacePage({ title, description, templateVariable
462
496
  setLabelId(undefined);
463
497
  setSmartView('all');
464
498
  }, onFolderChange: (value, ownerAccountId) => {
499
+ closeMailboxNavigation();
465
500
  if (value === folderId && ownerAccountId === folderAccountId)
466
501
  return;
467
502
  cancelRequests();
@@ -470,6 +505,7 @@ export default function MailWorkspacePage({ title, description, templateVariable
470
505
  setLabelId(undefined);
471
506
  setSmartView('all');
472
507
  }, onLabelChange: (value) => {
508
+ closeMailboxNavigation();
473
509
  if (value === labelId)
474
510
  return;
475
511
  cancelRequests();
@@ -478,6 +514,7 @@ export default function MailWorkspacePage({ title, description, templateVariable
478
514
  setFolderAccountId(undefined);
479
515
  setSmartView('all');
480
516
  }, onSmartViewChange: (value) => {
517
+ closeMailboxNavigation();
481
518
  if (value === smartView)
482
519
  return;
483
520
  cancelRequests();
@@ -513,11 +550,13 @@ export default function MailWorkspacePage({ title, description, templateVariable
513
550
  ? t('workspace.syncing', {
514
551
  defaultValue: 'Synchronizing…',
515
552
  })
516
- : syncLabel })] })] }) }), _jsxs("div", { className: 'flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-xl border bg-background', children: [_jsx("div", { className: 'shrink-0 border-b p-4', children: _jsxs("label", { className: 'relative block w-full min-w-0 sm:w-64', children: [_jsx(Search, { "aria-hidden": 'true', className: 'absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground' }), _jsx(Input, { "aria-label": t('workspace.search', {
517
- defaultValue: 'Search mail',
518
- }), className: 'pl-9', onChange: (event) => setQuery(event.target.value), placeholder: t('workspace.search', {
519
- defaultValue: 'Search mail',
520
- }), value: query })] }) }), error ? (_jsxs("div", { role: 'alert', className: 'flex items-center gap-3 border-b bg-destructive/5 px-4 py-2 text-sm text-destructive', children: [_jsx("span", { className: 'flex-1', children: error }), _jsx(Button, { onClick: () => {
553
+ : syncLabel })] }), accountsHref && accounts.length > 0 ? (_jsx(Button, { render: _jsx("a", { href: resolveAppUrl(accountsHref) }), nativeButton: false, role: 'link', variant: 'outline', children: t('navigation.accounts', { defaultValue: 'Mail accounts' }) })) : null, headerActions] }) }), _jsxs("div", { ref: workspaceRef, "data-slot": 'mail-workspace', className: '@container/mail flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-xl border bg-background', children: [_jsxs("div", { className: 'flex shrink-0 flex-wrap items-center gap-2 border-b p-4', children: [accounts.length > 0 ? (_jsxs(Button, { ref: navigationButtonRef, variant: 'outline', className: '@min-[56rem]/mail:hidden', "aria-haspopup": 'dialog', "aria-expanded": navigationOpen, onClick: () => setNavigationOpen(true), children: [_jsx(Mailbox, { "aria-hidden": 'true', className: 'size-4' }), t('workspace.mailboxNavigation', {
554
+ defaultValue: 'Mailboxes and folders',
555
+ })] })) : null, _jsxs("label", { className: 'relative block w-full min-w-0 @min-[32rem]/mail:w-64', children: [_jsx(Search, { "aria-hidden": 'true', className: 'absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground' }), _jsx(Input, { "aria-label": t('workspace.search', {
556
+ defaultValue: 'Search mail',
557
+ }), className: 'pl-9', onChange: (event) => setQuery(event.target.value), placeholder: t('workspace.search', {
558
+ defaultValue: 'Search mail',
559
+ }), value: query })] })] }), error ? (_jsxs("div", { role: 'alert', className: 'flex items-center gap-3 border-b bg-destructive/5 px-4 py-2 text-sm text-destructive', children: [_jsx("span", { className: 'flex-1', children: error }), _jsx(Button, { onClick: () => {
521
560
  loadAccounts();
522
561
  setReloadVersion((version) => version + 1);
523
562
  }, variant: 'ghost', children: t('workspace.retry', { defaultValue: 'Try again' }) })] })) : null, notice ? (_jsxs("div", { role: 'status', className: 'flex items-center gap-3 border-b bg-primary/5 px-4 py-2 text-sm', children: [_jsx("span", { className: 'flex-1', children: notice }), _jsx(Button, { variant: 'ghost', onClick: () => setNotice(undefined), children: t('workspace.dismiss', { defaultValue: 'Dismiss' }) })] })) : null, loadingAccounts && accounts.length === 0 ? (_jsxs("div", { role: 'status', className: 'flex min-h-64 flex-1 items-center justify-center gap-2 text-sm text-muted-foreground', children: [_jsx(RefreshCw, { "aria-hidden": 'true', className: 'size-4 animate-spin' }), t('workspace.loading', { defaultValue: 'Loading messages…' })] })) : accounts.length === 0 ? (_jsx("div", { className: 'grid flex-1 place-items-center p-8', children: _jsxs("div", { className: 'max-w-md space-y-4 text-center', children: [_jsx("span", { className: 'mx-auto grid size-14 place-items-center rounded-2xl bg-muted', children: _jsx(Inbox, { "aria-hidden": 'true', className: 'size-6 text-muted-foreground' }) }), _jsxs("div", { children: [_jsx("h2", { className: 'text-lg font-semibold text-foreground', children: t('workspace.noAccountsTitle', {
@@ -526,7 +565,14 @@ export default function MailWorkspacePage({ title, description, templateVariable
526
565
  defaultValue: 'Connect a mail account to sync messages, search conversations, and send from this workspace.',
527
566
  }) })] }), accountsHref ? (_jsxs(Button, { render: _jsx("a", { href: resolveAppUrl(accountsHref) }), nativeButton: false, role: 'link', children: [_jsx(PenLine, { "aria-hidden": 'true', className: 'size-4' }), t('workspace.connectAccount', {
528
567
  defaultValue: 'Connect mail account',
529
- })] })) : null] }) })) : (_jsxs("div", { className: 'grid min-h-0 flex-1 grid-cols-[13rem_20rem_minmax(0,1fr)] overflow-hidden', children: [_jsx("div", { className: 'min-h-0', children: mailboxNavigation }), _jsxs("div", { className: 'flex min-h-0 flex-col border-r', children: [_jsx("div", { className: 'flex shrink-0 items-center justify-between gap-3 border-b bg-muted/20 px-4 py-3', children: _jsx("h2", { className: 'truncate text-sm font-semibold', children: viewTitle }) }), _jsx(MailMessageList, { accountNames: accountNames, availableLabels: customLabels, labels: {
568
+ })] })) : null] }) })) : (_jsxs("div", { className: 'grid min-h-0 min-w-0 flex-1 grid-cols-1 overflow-hidden @min-[56rem]/mail:grid-cols-[13rem_20rem_minmax(0,1fr)]', children: [_jsx("div", { "data-slot": 'mail-navigation-pane', className: 'hidden min-h-0 @min-[56rem]/mail:block', children: mailboxNavigation }), _jsxs("div", { "data-slot": 'mail-list-pane', onClickCapture: (event) => {
569
+ if (!(event.target instanceof Element))
570
+ return;
571
+ const row = event.target.closest('button');
572
+ const list = event.currentTarget.querySelector('section');
573
+ if (row instanceof HTMLButtonElement && list?.contains(row))
574
+ selectedRowRef.current = row;
575
+ }, className: cn('min-h-0 min-w-0 flex-col @min-[56rem]/mail:flex @min-[56rem]/mail:border-r', selected ? 'hidden' : 'flex'), children: [_jsx("div", { className: 'flex shrink-0 items-center justify-between gap-3 border-b bg-muted/20 px-4 py-3', children: _jsx("h2", { className: 'truncate text-sm font-semibold', children: viewTitle }) }), _jsx(MailMessageList, { accountNames: accountNames, availableLabels: customLabels, labels: {
530
576
  loading: t('workspace.loading', {
531
577
  defaultValue: 'Loading messages…',
532
578
  }),
@@ -569,168 +615,176 @@ export default function MailWorkspacePage({ title, description, templateVariable
569
615
  defaultValue: 'Next page',
570
616
  }), title: t('workspace.nextPage', {
571
617
  defaultValue: 'Next page',
572
- }), type: 'button', onClick: () => changeMessagePage(pageIndex + 1), children: _jsx(ChevronRight, { "aria-hidden": 'true', className: 'size-3.5' }) })] })] })] }), _jsx("div", { className: 'flex min-h-0 min-w-0 flex-col', children: _jsx(MailConversationView, { availableLabels: selectedMessageCanUseLabels ? customLabels : [], actions: selectedMessageAccount?.status === 'active'
573
- ? {
574
- archive: selectedMessageCanMove
575
- ? (message) => {
576
- const archive = foldersByAccountId
577
- .get(message.accountId)
578
- ?.find((folder) => folder.type === 'archive');
579
- if (archive)
580
- mutateMessage(mail.moveMessage({
581
- accountId: message.accountId,
582
- messageId: message.id,
583
- providerFolderId: archive.providerFolderId,
584
- }), true);
585
- }
586
- : undefined,
587
- delete: (message) => deleteWorkspaceMessage(message),
588
- canDelete: (message) => Boolean(!message.scheduledSend &&
589
- (selectedMessageProviderCapabilities?.moveMessage ||
590
- isMessageInTrash(message, foldersByAccountId) ||
591
- message.providerMessageId.startsWith('local-draft:'))),
592
- downloadAttachment: (message, attachment) => {
593
- void downloadAttachment(mail, message, attachment).catch((cause) => setError(mailErrorMessage(cause, t('workspace.downloadFailed', {
594
- defaultValue: 'Unable to download attachment.',
595
- }))));
596
- },
597
- reply: (message) => {
598
- if (preparingReplyRef.current || composerRequest)
599
- return;
600
- preparingReplyRef.current = true;
601
- void prepareReplyContent(mail, message)
602
- .then(({ message: source, quote, uploads }) => {
603
- openComposer({
604
- ...EMPTY_COMPOSER,
605
- mode: 'reply',
606
- forwardQuote: quote,
607
- relatedMessageId: source.id,
608
- to: source.replyTo.length
609
- ? source.replyTo
610
- .map((address) => address.address)
611
- .join(', ')
612
- : (source.from?.address ?? ''),
613
- subject: replySubject(source.subject),
614
- }, [], source.accountId, uploads);
615
- })
616
- .catch(requestError)
617
- .finally(() => {
618
- preparingReplyRef.current = false;
619
- });
620
- },
621
- forward: (message) => openComposer({
622
- ...EMPTY_COMPOSER,
623
- mode: 'forward',
624
- relatedMessageId: message.id,
625
- subject: forwardSubject(message.subject),
626
- forwardQuote: createForwardQuote(message),
627
- forwardBodyIncluded: true,
628
- }, [], message.accountId),
629
- cancelScheduled: (message) => editScheduledDraft(message, () => { }),
630
- editDraft: selectedMessageCanDraft
631
- ? (message) => editScheduledDraft(message, (message) => openComposer({
618
+ }), type: 'button', onClick: () => changeMessagePage(pageIndex + 1), children: _jsx(ChevronRight, { "aria-hidden": 'true', className: 'size-3.5' }) })] })] })] }), _jsxs("div", { "data-slot": 'mail-conversation-pane', className: cn('min-h-0 min-w-0 flex-col @min-[56rem]/mail:flex', selected ? 'flex' : 'hidden'), children: [_jsxs(Button, { ref: conversationBackRef, variant: 'ghost', className: 'm-2 shrink-0 self-start @min-[56rem]/mail:hidden', onClick: () => {
619
+ clearSelection();
620
+ window.requestAnimationFrame(() => {
621
+ if (selectedRowRef.current?.isConnected)
622
+ selectedRowRef.current.focus({ preventScroll: true });
623
+ });
624
+ }, children: [_jsx(ChevronLeft, { "aria-hidden": 'true', className: 'size-4' }), t('workspace.backToMessages', {
625
+ defaultValue: 'Back to message list',
626
+ })] }), _jsx(MailConversationView, { availableLabels: selectedMessageCanUseLabels ? customLabels : [], actions: selectedMessageAccount?.status === 'active'
627
+ ? {
628
+ archive: selectedMessageCanMove
629
+ ? (message) => {
630
+ const archive = foldersByAccountId
631
+ .get(message.accountId)
632
+ ?.find((folder) => folder.type === 'archive');
633
+ if (archive)
634
+ mutateMessage(mail.moveMessage({
635
+ accountId: message.accountId,
636
+ messageId: message.id,
637
+ providerFolderId: archive.providerFolderId,
638
+ }), true);
639
+ }
640
+ : undefined,
641
+ delete: (message) => deleteWorkspaceMessage(message),
642
+ canDelete: (message) => Boolean(!message.scheduledSend &&
643
+ (selectedMessageProviderCapabilities?.moveMessage ||
644
+ isMessageInTrash(message, foldersByAccountId) ||
645
+ message.providerMessageId.startsWith('local-draft:'))),
646
+ downloadAttachment: (message, attachment) => {
647
+ void downloadAttachment(mail, message, attachment).catch((cause) => setError(mailErrorMessage(cause, t('workspace.downloadFailed', {
648
+ defaultValue: 'Unable to download attachment.',
649
+ }))));
650
+ },
651
+ reply: (message) => {
652
+ if (preparingReplyRef.current || composerRequest)
653
+ return;
654
+ preparingReplyRef.current = true;
655
+ void prepareMailReply(mail, message)
656
+ .then((reply) => {
657
+ openComposer(reply.value, reply.attachments, reply.accountId, reply.uploads);
658
+ })
659
+ .catch(requestError)
660
+ .finally(() => {
661
+ preparingReplyRef.current = false;
662
+ });
663
+ },
664
+ forward: (message) => openComposer({
632
665
  ...EMPTY_COMPOSER,
633
- mode: 'edit',
634
- draftMessageId: message.id,
635
- draftRevision: message.draftRevision,
636
- draftSource: message.draftSource,
637
- fromAddress: message.from?.address,
638
- to: formatAddressList(message.to),
639
- cc: formatAddressList(message.cc),
640
- bcc: formatAddressList(message.bcc),
641
- subject: message.subject,
642
- ...readDraftComposerBody(message),
643
- draftConflict: message.draftConflict,
644
- }, message.attachments, message.accountId))
645
- : undefined,
646
- toggleRead: (message) => mutateMessage(setMessageRead(message, !message.read)),
647
- toggleStarred: (message) => mutateMessage(mail.updateMessage({
648
- accountId: message.accountId,
649
- messageId: message.id,
650
- starred: !message.starred,
651
- })),
652
- toggleTodo: (message) => mutateMessage(mail.updateMessage({
653
- accountId: message.accountId,
654
- messageId: message.id,
655
- todo: !message.todo,
656
- })),
657
- saveNote: (message, note) => mutateMessage(mail.updateMessage({
658
- accountId: message.accountId,
659
- messageId: message.id,
660
- note: note.trim() || null,
661
- })),
662
- toggleLabel: selectedMessageCanUseLabels
663
- ? (message, labelId, assigned) => mutateMessage(mail.updateMessageLabels({
666
+ mode: 'forward',
667
+ relatedMessageId: message.id,
668
+ subject: forwardSubject(message.subject),
669
+ forwardQuote: createForwardQuote(message),
670
+ forwardBodyIncluded: true,
671
+ }, [], message.accountId),
672
+ cancelScheduled: (message) => editScheduledDraft(message, () => { }),
673
+ editDraft: selectedMessageCanDraft
674
+ ? (message) => editScheduledDraft(message, (message) => openComposer({
675
+ ...EMPTY_COMPOSER,
676
+ mode: 'edit',
677
+ draftMessageId: message.id,
678
+ draftRevision: message.draftRevision,
679
+ draftSource: message.draftSource,
680
+ fromAddress: message.from?.address,
681
+ to: formatAddressList(message.to),
682
+ cc: formatAddressList(message.cc),
683
+ bcc: formatAddressList(message.bcc),
684
+ subject: message.subject,
685
+ ...readDraftComposerBody(message),
686
+ draftConflict: message.draftConflict,
687
+ }, message.attachments, message.accountId))
688
+ : undefined,
689
+ toggleRead: (message) => mutateMessage(setMessageRead(message, !message.read)),
690
+ toggleStarred: (message) => mutateMessage(mail.updateMessage({
664
691
  accountId: message.accountId,
665
692
  messageId: message.id,
666
- addLabelIds: assigned ? [labelId] : [],
667
- removeLabelIds: assigned ? [] : [labelId],
668
- }))
669
- : undefined,
670
- }
671
- : undefined, actionLabels: {
672
- archive: t('workspace.archive', { defaultValue: 'Archive' }),
673
- delete: selectedMessageInTrash
674
- ? t('workspace.permanentlyDelete', {
675
- defaultValue: 'Permanently delete',
676
- })
677
- : t('workspace.delete', { defaultValue: 'Delete' }),
678
- download: t('workspace.download', {
679
- defaultValue: 'Download',
680
- }),
681
- reply: t('workspace.reply', { defaultValue: 'Reply' }),
682
- forward: t('workspace.forward', { defaultValue: 'Forward' }),
683
- editDraft: t('workspace.editDraft', {
684
- defaultValue: 'Edit draft',
685
- }),
686
- markRead: t('workspace.markRead', {
687
- defaultValue: 'Mark read',
688
- }),
689
- markUnread: t('workspace.markUnread', {
690
- defaultValue: 'Mark unread',
691
- }),
692
- star: t('workspace.star', { defaultValue: 'Star' }),
693
- unstar: t('workspace.unstar', {
694
- defaultValue: 'Remove star',
695
- }),
696
- collapseMessage: t('workspace.collapseMessage', {
697
- defaultValue: 'Collapse message',
698
- }),
699
- expandMessage: t('workspace.expandMessage', {
700
- defaultValue: 'Expand message',
701
- }),
702
- }, labels: {
703
- attachmentCount: (count) => t('workspace.attachmentCount', {
704
- count,
705
- defaultValue: '{{count}} attachments',
706
- }),
707
- conversation: (count) => t('workspace.conversation', {
708
- count,
709
- defaultValue: '{{count}} messages in this conversation',
710
- }),
711
- loadMore: t('workspace.loadEarlier', {
712
- defaultValue: 'Load earlier messages',
713
- }),
714
- noSubject: t('workspace.noSubject', {
715
- defaultValue: '(no subject)',
716
- }),
717
- selectMessage: t('workspace.selectMessage', {
718
- defaultValue: 'Select a message to read it.',
719
- }),
720
- unknownSender: t('workspace.unknownSender', {
721
- defaultValue: 'Unknown sender',
722
- }),
723
- labels: t('workspace.labels', { defaultValue: 'Labels' }),
724
- note: t('workspace.note', { defaultValue: 'Note' }),
725
- notePlaceholder: t('workspace.notePlaceholder', {
726
- defaultValue: 'Add a private note…',
727
- }),
728
- saveNote: t('workspace.saveNote', {
729
- defaultValue: 'Save note',
730
- }),
731
- todo: t('workspace.todo', { defaultValue: 'To do' }),
732
- more: t('workspace.more', { defaultValue: 'More actions' }),
733
- }, loading: loadingConversation, messages: conversation, nextCursor: conversationCursor, onLoadMore: loadMoreConversation, subject: selected?.subject }) })] })), composerRequest ? (_jsx(MailWorkspaceComposer, { onSelectAccount: rememberComposeAccount, request: composerRequest, accounts: accounts, providers: providers, templateVariables: templateVariables, onClose: () => setComposerRequest(undefined), onComplete: (result, rejectedRecipients, operationError) => {
693
+ starred: !message.starred,
694
+ })),
695
+ toggleTodo: (message) => mutateMessage(mail.updateMessage({
696
+ accountId: message.accountId,
697
+ messageId: message.id,
698
+ todo: !message.todo,
699
+ })),
700
+ saveNote: (message, note) => mutateMessage(mail.updateMessage({
701
+ accountId: message.accountId,
702
+ messageId: message.id,
703
+ note: note.trim() || null,
704
+ })),
705
+ toggleLabel: selectedMessageCanUseLabels
706
+ ? (message, labelId, assigned) => mutateMessage(mail.updateMessageLabels({
707
+ accountId: message.accountId,
708
+ messageId: message.id,
709
+ addLabelIds: assigned ? [labelId] : [],
710
+ removeLabelIds: assigned ? [] : [labelId],
711
+ }))
712
+ : undefined,
713
+ }
714
+ : undefined, actionLabels: {
715
+ archive: t('workspace.archive', { defaultValue: 'Archive' }),
716
+ delete: selectedMessageInTrash
717
+ ? t('workspace.permanentlyDelete', {
718
+ defaultValue: 'Permanently delete',
719
+ })
720
+ : t('workspace.delete', { defaultValue: 'Delete' }),
721
+ download: t('workspace.download', {
722
+ defaultValue: 'Download',
723
+ }),
724
+ reply: t('workspace.reply', { defaultValue: 'Reply' }),
725
+ forward: t('workspace.forward', { defaultValue: 'Forward' }),
726
+ editDraft: t('workspace.editDraft', {
727
+ defaultValue: 'Edit draft',
728
+ }),
729
+ markRead: t('workspace.markRead', {
730
+ defaultValue: 'Mark read',
731
+ }),
732
+ markUnread: t('workspace.markUnread', {
733
+ defaultValue: 'Mark unread',
734
+ }),
735
+ star: t('workspace.star', { defaultValue: 'Star' }),
736
+ unstar: t('workspace.unstar', {
737
+ defaultValue: 'Remove star',
738
+ }),
739
+ collapseMessage: t('workspace.collapseMessage', {
740
+ defaultValue: 'Collapse message',
741
+ }),
742
+ expandMessage: t('workspace.expandMessage', {
743
+ defaultValue: 'Expand message',
744
+ }),
745
+ }, labels: {
746
+ attachmentCount: (count) => t('workspace.attachmentCount', {
747
+ count,
748
+ defaultValue: '{{count}} attachments',
749
+ }),
750
+ conversation: (count) => t('workspace.conversation', {
751
+ count,
752
+ defaultValue: '{{count}} messages in this conversation',
753
+ }),
754
+ loadMore: t('workspace.loadEarlier', {
755
+ defaultValue: 'Load earlier messages',
756
+ }),
757
+ noSubject: t('workspace.noSubject', {
758
+ defaultValue: '(no subject)',
759
+ }),
760
+ selectMessage: t('workspace.selectMessage', {
761
+ defaultValue: 'Select a message to read it.',
762
+ }),
763
+ unknownSender: t('workspace.unknownSender', {
764
+ defaultValue: 'Unknown sender',
765
+ }),
766
+ labels: t('workspace.labels', { defaultValue: 'Labels' }),
767
+ note: t('workspace.note', { defaultValue: 'Note' }),
768
+ notePlaceholder: t('workspace.notePlaceholder', {
769
+ defaultValue: 'Add a private note…',
770
+ }),
771
+ saveNote: t('workspace.saveNote', {
772
+ defaultValue: 'Save note',
773
+ }),
774
+ todo: t('workspace.todo', { defaultValue: 'To do' }),
775
+ more: t('workspace.more', { defaultValue: 'More actions' }),
776
+ }, loading: loadingConversation, messages: conversation, nextCursor: conversationCursor, onLoadMore: loadMoreConversation, subject: selected?.subject })] })] })), _jsx(Sheet, { open: navigationOpen, onOpenChange: setNavigationOpen, children: _jsxs(SheetContent, { side: 'left', closeLabel: t('workspace.closeNavigation', {
777
+ defaultValue: 'Close mailbox navigation',
778
+ }), finalFocus: () => navigationButtonRef.current?.getClientRects().length
779
+ ? navigationButtonRef.current
780
+ : workspaceRef.current?.querySelector('input'), children: [_jsx(SheetHeader, { className: 'pr-14', children: _jsx(SheetTitle, { children: t('workspace.mailboxNavigation', {
781
+ defaultValue: 'Mailboxes and folders',
782
+ }) }) }), _jsx("div", { className: 'min-h-0 flex-1 overflow-y-auto', onClickCapture: (event) => {
783
+ // Current folders can return early inside the reused sidebar itself.
784
+ if (event.target instanceof Element &&
785
+ event.target.closest('button[aria-current="page"]'))
786
+ closeMailboxNavigation();
787
+ }, children: mailboxNavigation })] }) }), composerRequest ? (_jsx(MailWorkspaceComposer, { onSelectAccount: rememberComposeAccount, request: composerRequest, accounts: accounts, providers: providers, templateVariables: templateVariables, onClose: () => setComposerRequest(undefined), onComplete: (result, rejectedRecipients, operationError) => {
734
788
  setReloadVersion((version) => version + 1);
735
789
  const notice = result === 'partial'
736
790
  ? t('workspace.submissionPartial', {
@@ -813,9 +867,6 @@ function isMessageInTrash(message, foldersByAccountId) {
813
867
  function formatAddressList(addresses) {
814
868
  return addresses.map((address) => address.address).join(', ');
815
869
  }
816
- function replySubject(subject) {
817
- return /^re:/iu.test(subject.trim()) ? subject : `Re: ${subject}`;
818
- }
819
870
  function forwardSubject(subject) {
820
871
  return /^fwd?:/iu.test(subject.trim()) ? subject : `Fwd: ${subject}`;
821
872
  }