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

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 (122) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +35 -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/components/ui/field.d.ts +7 -0
  14. package/dist/client/components/ui/field.d.ts.map +1 -0
  15. package/dist/client/components/ui/field.js +20 -0
  16. package/dist/client/components/ui/field.js.map +1 -0
  17. package/dist/client/contracts/composer.d.ts +29 -2
  18. package/dist/client/contracts/composer.d.ts.map +1 -1
  19. package/dist/client/contracts/composer.js.map +1 -1
  20. package/dist/client/hooks/use-mail-composer.d.ts +1 -1
  21. package/dist/client/hooks/use-mail-composer.d.ts.map +1 -1
  22. package/dist/client/hooks/use-mail-composer.js +54 -10
  23. package/dist/client/hooks/use-mail-composer.js.map +1 -1
  24. package/dist/client/hooks/use-mail-unread-count.d.ts +13 -0
  25. package/dist/client/hooks/use-mail-unread-count.d.ts.map +1 -0
  26. package/dist/client/hooks/use-mail-unread-count.js +106 -0
  27. package/dist/client/hooks/use-mail-unread-count.js.map +1 -0
  28. package/dist/client/hooks/use-mail-workspace-data.d.ts +3 -1
  29. package/dist/client/hooks/use-mail-workspace-data.d.ts.map +1 -1
  30. package/dist/client/hooks/use-mail-workspace-data.js +14 -2
  31. package/dist/client/hooks/use-mail-workspace-data.js.map +1 -1
  32. package/dist/client/index.d.ts +3 -0
  33. package/dist/client/index.d.ts.map +1 -1
  34. package/dist/client/index.js +2 -0
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/lib/mail-composer-completion.d.ts +5 -0
  37. package/dist/client/lib/mail-composer-completion.d.ts.map +1 -0
  38. package/dist/client/lib/mail-composer-completion.js +28 -0
  39. package/dist/client/lib/mail-composer-completion.js.map +1 -0
  40. package/dist/client/locales/en-US.d.ts +4 -0
  41. package/dist/client/locales/en-US.d.ts.map +1 -1
  42. package/dist/client/locales/en-US.js +7 -3
  43. package/dist/client/locales/en-US.js.map +1 -1
  44. package/dist/client/locales/zh-CN.d.ts.map +1 -1
  45. package/dist/client/locales/zh-CN.js +6 -2
  46. package/dist/client/locales/zh-CN.js.map +1 -1
  47. package/dist/client/mail-client.d.ts +5 -1
  48. package/dist/client/mail-client.d.ts.map +1 -1
  49. package/dist/client/mail-client.js.map +1 -1
  50. package/dist/client/pages/mail-accounts-page.d.ts.map +1 -1
  51. package/dist/client/pages/mail-accounts-page.js +4 -1
  52. package/dist/client/pages/mail-accounts-page.js.map +1 -1
  53. package/dist/client/pages/mail-workspace-page.d.ts +4 -2
  54. package/dist/client/pages/mail-workspace-page.d.ts.map +1 -1
  55. package/dist/client/pages/mail-workspace-page.js +253 -173
  56. package/dist/client/pages/mail-workspace-page.js.map +1 -1
  57. package/dist/client/prepare-mail-reply.d.ts +9 -0
  58. package/dist/client/prepare-mail-reply.d.ts.map +1 -0
  59. package/dist/client/prepare-mail-reply.js +28 -0
  60. package/dist/client/prepare-mail-reply.js.map +1 -0
  61. package/dist/database/migrations/.manifest.json +6 -2
  62. package/dist/database/migrations/202609030001_create_mail_tables.d.ts.map +1 -1
  63. package/dist/database/migrations/202609030001_create_mail_tables.js +8 -4
  64. package/dist/database/migrations/202609030001_create_mail_tables.js.map +1 -1
  65. package/dist/database/migrations/202610090001_mail_create_message_participants.d.ts +4 -0
  66. package/dist/database/migrations/202610090001_mail_create_message_participants.d.ts.map +1 -0
  67. package/dist/database/migrations/202610090001_mail_create_message_participants.js +167 -0
  68. package/dist/database/migrations/202610090001_mail_create_message_participants.js.map +1 -0
  69. package/dist/server/config.d.ts +1 -1
  70. package/dist/server/config.d.ts.map +1 -1
  71. package/dist/server/config.js +17 -9
  72. package/dist/server/config.js.map +1 -1
  73. package/dist/server/locales/en-US.d.ts +1 -0
  74. package/dist/server/locales/en-US.d.ts.map +1 -1
  75. package/dist/server/locales/en-US.js +1 -0
  76. package/dist/server/locales/en-US.js.map +1 -1
  77. package/dist/server/locales/zh-CN.d.ts.map +1 -1
  78. package/dist/server/locales/zh-CN.js +1 -0
  79. package/dist/server/locales/zh-CN.js.map +1 -1
  80. package/dist/server/providers/mail-core.d.ts +2 -0
  81. package/dist/server/providers/mail-core.d.ts.map +1 -1
  82. package/dist/server/providers/mail-core.js +39 -1
  83. package/dist/server/providers/mail-core.js.map +1 -1
  84. package/dist/server/routes/api.d.ts.map +1 -1
  85. package/dist/server/routes/api.js +6 -2
  86. package/dist/server/routes/api.js.map +1 -1
  87. package/dist/server/routes/schemas.d.ts +2 -0
  88. package/dist/server/routes/schemas.d.ts.map +1 -1
  89. package/dist/server/routes/schemas.js +13 -0
  90. package/dist/server/routes/schemas.js.map +1 -1
  91. package/dist/server/services/authorization.js +1 -1
  92. package/dist/server/services/authorization.js.map +1 -1
  93. package/dist/server/store/account-removals.d.ts.map +1 -1
  94. package/dist/server/store/account-removals.js +6 -0
  95. package/dist/server/store/account-removals.js.map +1 -1
  96. package/dist/server/store/draft-states.d.ts.map +1 -1
  97. package/dist/server/store/draft-states.js +2 -0
  98. package/dist/server/store/draft-states.js.map +1 -1
  99. package/dist/server/store/message-participants.d.ts +25 -0
  100. package/dist/server/store/message-participants.d.ts.map +1 -0
  101. package/dist/server/store/message-participants.js +109 -0
  102. package/dist/server/store/message-participants.js.map +1 -0
  103. package/dist/server/store/message-writes.d.ts.map +1 -1
  104. package/dist/server/store/message-writes.js +3 -0
  105. package/dist/server/store/message-writes.js.map +1 -1
  106. package/dist/server/store/messages.d.ts.map +1 -1
  107. package/dist/server/store/messages.js +15 -0
  108. package/dist/server/store/messages.js.map +1 -1
  109. package/dist/shared/mail.d.ts +3 -1
  110. package/dist/shared/mail.d.ts.map +1 -1
  111. package/dist/shared/mail.js.map +1 -1
  112. package/dist/shared/participant.d.ts +14 -0
  113. package/dist/shared/participant.d.ts.map +1 -0
  114. package/dist/shared/participant.js +45 -0
  115. package/dist/shared/participant.js.map +1 -0
  116. package/package.json +10 -7
  117. package/skills/nocobase-app-plugin-mail/SKILL.md +9 -7
  118. package/skills/nocobase-app-plugin-mail/references/client-integration.md +125 -1
  119. package/skills/nocobase-app-plugin-mail/references/configuration-and-accounts.md +10 -4
  120. package/skills/nocobase-app-plugin-mail/references/http-api.md +32 -24
  121. package/skills/nocobase-app-plugin-mail/references/public-api.md +27 -20
  122. package/skills/nocobase-app-plugin-mail/references/synchronization-and-diagnostics.md +14 -0
@@ -1,19 +1,23 @@
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';
10
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
9
+ import { ChevronLeft, ChevronRight, Mailbox, Inbox, PenLine, RefreshCw, } from 'lucide-react';
10
+ import { useCallback, useEffect, useId, useMemo, useRef, useState, } from 'react';
11
11
  import { useTranslation } from '@nocobase/i18n/client';
12
12
  import { resolveAppUrl } from '@nocobase/app-client';
13
13
  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 { Field, FieldDescription, FieldError, FieldGroup, FieldLabel, } from '../components/ui/field.js';
19
+ import { cn } from '../lib/utils.js';
20
+ import { parseMailParticipant } from '../../shared/participant.js';
17
21
  import { getMailFolderDisplayName, getMailFolderSections, getVirtualMailFolders, } from '../lib/mail-folders.js';
18
22
  import { mailErrorMessage, } from '../mail-client.js';
19
23
  import { useMailClient } from '../runtime.js';
@@ -21,7 +25,7 @@ import { mailSyncDateMonthsAgo } from '../../shared/mail-sync-date.js';
21
25
  import { MAIL_VIRTUAL_FOLDER_IDS } from '../../shared/mail.js';
22
26
  import { MAIL_PLUGIN_NS } from '../namespace.js';
23
27
  const LAST_COMPOSE_ACCOUNT_KEY_PREFIX = 'nocobase:mail:last-compose-account:v1:';
24
- export default function MailWorkspacePage({ title, description, templateVariables = {}, accountsHref, } = {}) {
28
+ export default function MailWorkspacePage({ title, description, accountsHref, headerActions, templateVariables = {}, } = {}) {
25
29
  const { t } = useTranslation(MAIL_PLUGIN_NS);
26
30
  const mail = useMailClient();
27
31
  const [accounts, setAccounts] = useState([]);
@@ -35,6 +39,11 @@ export default function MailWorkspacePage({ title, description, templateVariable
35
39
  const [labelId, setLabelId] = useState();
36
40
  const [smartView, setSmartView] = useState('all');
37
41
  const [query, setQuery] = useState('');
42
+ const [participant, setParticipant] = useState('');
43
+ const [debouncedParticipant, setDebouncedParticipant] = useState('');
44
+ const filterId = useId();
45
+ const participantInvalid = participant !== '' && !parseMailParticipant(participant);
46
+ const participantReady = participant === debouncedParticipant && !participantInvalid;
38
47
  const [loadingAccounts, setLoadingAccounts] = useState(true);
39
48
  const [syncRuns, setSyncRuns] = useState({});
40
49
  const syncRunsRef = useRef({});
@@ -46,6 +55,24 @@ export default function MailWorkspacePage({ title, description, templateVariable
46
55
  const [debouncedQuery, setDebouncedQuery] = useState('');
47
56
  const [composeAccountId, setComposeAccountId] = useState('');
48
57
  const [composerRequest, setComposerRequest] = useState();
58
+ const [navigationOpen, setNavigationOpen] = useState(false);
59
+ const workspaceRef = useRef(null);
60
+ const navigationButtonRef = useRef(null);
61
+ const conversationBackRef = useRef(null);
62
+ const selectedRowRef = useRef(null);
63
+ useEffect(() => {
64
+ if (!navigationOpen ||
65
+ !navigationButtonRef.current ||
66
+ typeof ResizeObserver === 'undefined')
67
+ return;
68
+ // CSS owns the breakpoint; observing its trigger also handles root font changes.
69
+ const observer = new ResizeObserver(() => {
70
+ if (!navigationButtonRef.current?.getClientRects().length)
71
+ setNavigationOpen(false);
72
+ });
73
+ observer.observe(navigationButtonRef.current);
74
+ return () => observer.disconnect();
75
+ }, [navigationOpen]);
49
76
  useEffect(() => {
50
77
  syncRunsRef.current = syncRuns;
51
78
  }, [syncRuns]);
@@ -57,19 +84,37 @@ export default function MailWorkspacePage({ title, description, templateVariable
57
84
  folderId,
58
85
  labelId,
59
86
  q: debouncedQuery.trim() || undefined,
87
+ ...(debouncedParticipant ? { participant: debouncedParticipant } : {}),
60
88
  unread: smartView === 'unread' ? true : undefined,
61
89
  starred: smartView === 'starred' ? true : undefined,
62
90
  pageSize: 50,
63
- }), [accountId, debouncedQuery, folderAccountId, folderId, labelId, smartView]);
91
+ }), [
92
+ accountId,
93
+ debouncedQuery,
94
+ debouncedParticipant,
95
+ folderAccountId,
96
+ folderId,
97
+ labelId,
98
+ smartView,
99
+ ]);
64
100
  const { messages, total, nextCursor, pageIndex, listVersion, selected, conversation, conversationCursor, loadingMessages, loadingConversation, selectMessage, changeMessagePage, loadMoreConversation, updateVisibleMessage, setMessageRead, clearSelection, resetMailbox, cancelRequests, } = useMailWorkspaceData({
65
101
  mail,
66
102
  accounts,
67
103
  messageQuery,
104
+ queryEnabled: participantReady,
68
105
  reloadVersion,
69
106
  requestError,
70
107
  setError,
71
108
  onFocus: () => loadAccounts(false),
72
109
  });
110
+ const selectedMessageKey = selected
111
+ ? JSON.stringify([selected.accountId, selected.id])
112
+ : undefined;
113
+ useEffect(() => {
114
+ if (selectedMessageKey &&
115
+ navigationButtonRef.current?.getClientRects().length)
116
+ conversationBackRef.current?.focus({ preventScroll: true });
117
+ }, [selectedMessageKey]);
73
118
  const currentAccount = accounts.find((account) => account.id === accountId);
74
119
  const currentProviderCapabilities = findProviderCapabilities(currentAccount, providers);
75
120
  const composeAccount = accounts.find((account) => account.id === composeAccountId);
@@ -224,6 +269,10 @@ export default function MailWorkspacePage({ title, description, templateVariable
224
269
  const timer = window.setTimeout(() => setDebouncedQuery(query), 300);
225
270
  return () => window.clearTimeout(timer);
226
271
  }, [query]);
272
+ useEffect(() => {
273
+ const timer = window.setTimeout(() => setDebouncedParticipant(participant), 300);
274
+ return () => window.clearTimeout(timer);
275
+ }, [participant]);
227
276
  const folderNames = useMemo(() => ({
228
277
  inbox: t('workspace.defaultFolders.inbox', {
229
278
  defaultValue: 'Inbox',
@@ -436,6 +485,11 @@ export default function MailWorkspacePage({ title, description, templateVariable
436
485
  const syncLabel = isAllAccounts
437
486
  ? t('workspace.syncAll', { defaultValue: 'Sync all mailboxes' })
438
487
  : t('workspace.incrementalRefresh', { defaultValue: 'Sync mailbox' });
488
+ const closeMailboxNavigation = () => {
489
+ setNavigationOpen(false);
490
+ if (navigationOpen)
491
+ clearSelection();
492
+ };
439
493
  const mailboxNavigation = (_jsx(MailboxSidebar, { accountId: accountId, accounts: accounts, customLabels: customLabels, folderId: folderId, folderAccountId: folderAccountId, folders: folders, accountFolderGroups: accountFolderGroups, labelId: labelId, labels: {
440
494
  account: t('dev.account', { defaultValue: 'Account' }),
441
495
  allAccounts: t('workspace.allAccounts', {
@@ -449,6 +503,7 @@ export default function MailWorkspacePage({ title, description, templateVariable
449
503
  folderNames,
450
504
  systemFolderNames,
451
505
  }, onAccountChange: (value) => {
506
+ closeMailboxNavigation();
452
507
  if (value === accountId)
453
508
  return;
454
509
  cancelRequests();
@@ -462,6 +517,7 @@ export default function MailWorkspacePage({ title, description, templateVariable
462
517
  setLabelId(undefined);
463
518
  setSmartView('all');
464
519
  }, onFolderChange: (value, ownerAccountId) => {
520
+ closeMailboxNavigation();
465
521
  if (value === folderId && ownerAccountId === folderAccountId)
466
522
  return;
467
523
  cancelRequests();
@@ -470,6 +526,7 @@ export default function MailWorkspacePage({ title, description, templateVariable
470
526
  setLabelId(undefined);
471
527
  setSmartView('all');
472
528
  }, onLabelChange: (value) => {
529
+ closeMailboxNavigation();
473
530
  if (value === labelId)
474
531
  return;
475
532
  cancelRequests();
@@ -478,6 +535,7 @@ export default function MailWorkspacePage({ title, description, templateVariable
478
535
  setFolderAccountId(undefined);
479
536
  setSmartView('all');
480
537
  }, onSmartViewChange: (value) => {
538
+ closeMailboxNavigation();
481
539
  if (value === smartView)
482
540
  return;
483
541
  cancelRequests();
@@ -513,11 +571,21 @@ export default function MailWorkspacePage({ title, description, templateVariable
513
571
  ? t('workspace.syncing', {
514
572
  defaultValue: 'Synchronizing…',
515
573
  })
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: () => {
574
+ : 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: 'grid shrink-0 grid-cols-[auto_minmax(0,1fr)] items-start gap-2 border-b px-4 py-1.5 @min-[44rem]/mail:flex @min-[44rem]/mail:flex-wrap @min-[44rem]/mail:gap-4 @min-[44rem]/mail:py-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' }), _jsx("span", { className: 'sr-only @min-[44rem]/mail:not-sr-only', children: t('workspace.mailboxNavigation', {
575
+ defaultValue: 'Mailboxes and folders',
576
+ }) })] })) : null, _jsxs(FieldGroup, { className: 'contents @min-[44rem]/mail:flex @min-[44rem]/mail:min-w-0 @min-[44rem]/mail:flex-1 @min-[44rem]/mail:basis-full @min-[44rem]/mail:flex-row @min-[44rem]/mail:items-start @min-[44rem]/mail:gap-4 @min-[56rem]/mail:basis-auto', children: [_jsxs(Field, { className: 'col-start-2 min-w-0 @min-[44rem]/mail:w-64', children: [_jsx(FieldLabel, { className: 'sr-only @min-[44rem]/mail:not-sr-only', htmlFor: `${filterId}-search`, children: t('workspace.search', { defaultValue: 'Search mail' }) }), _jsx(Input, { id: `${filterId}-search`, onChange: (event) => setQuery(event.target.value), placeholder: t('workspace.search', {
577
+ defaultValue: 'Search mail',
578
+ }), value: query })] }), _jsxs(Field, { className: 'col-span-2 min-w-0 gap-1 @min-[44rem]/mail:w-80 @min-[44rem]/mail:gap-2', "data-invalid": participantInvalid, children: [_jsx(FieldLabel, { htmlFor: `${filterId}-participant`, children: t('workspace.participant', { defaultValue: 'Participant' }) }), _jsx(Input, { id: `${filterId}-participant`, "aria-invalid": participantInvalid, "aria-describedby": `${filterId}-participant-hint`, "aria-errormessage": participantInvalid
579
+ ? `${filterId}-participant-error`
580
+ : undefined, onChange: (event) => {
581
+ // Invalidate immediately, before the debounced replacement request.
582
+ resetMailbox();
583
+ setParticipant(event.target.value);
584
+ }, placeholder: 'alice@example.com / @example.com', value: participant }), _jsx(FieldDescription, { id: `${filterId}-participant-hint`, children: t('workspace.participantHint', {
585
+ defaultValue: 'Full email address or @domain. Matches From, To and Cc only.',
586
+ }) }), participantInvalid ? (_jsx(FieldError, { id: `${filterId}-participant-error`, children: t('workspace.participantInvalid', {
587
+ defaultValue: 'Enter a valid full email address or @domain.',
588
+ }) })) : null] })] })] }), 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
589
  loadAccounts();
522
590
  setReloadVersion((version) => version + 1);
523
591
  }, 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 +594,14 @@ export default function MailWorkspacePage({ title, description, templateVariable
526
594
  defaultValue: 'Connect a mail account to sync messages, search conversations, and send from this workspace.',
527
595
  }) })] }), 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
596
  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: {
597
+ })] })) : 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) => {
598
+ if (!(event.target instanceof Element))
599
+ return;
600
+ const row = event.target.closest('button');
601
+ const list = event.currentTarget.querySelector('section');
602
+ if (row instanceof HTMLButtonElement && list?.contains(row))
603
+ selectedRowRef.current = row;
604
+ }, 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
605
  loading: t('workspace.loading', {
531
606
  defaultValue: 'Loading messages…',
532
607
  }),
@@ -569,168 +644,176 @@ export default function MailWorkspacePage({ title, description, templateVariable
569
644
  defaultValue: 'Next page',
570
645
  }), title: t('workspace.nextPage', {
571
646
  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({
647
+ }), 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: () => {
648
+ clearSelection();
649
+ window.requestAnimationFrame(() => {
650
+ if (selectedRowRef.current?.isConnected)
651
+ selectedRowRef.current.focus({ preventScroll: true });
652
+ });
653
+ }, children: [_jsx(ChevronLeft, { "aria-hidden": 'true', className: 'size-4' }), t('workspace.backToMessages', {
654
+ defaultValue: 'Back to message list',
655
+ })] }), _jsx(MailConversationView, { availableLabels: selectedMessageCanUseLabels ? customLabels : [], actions: selectedMessageAccount?.status === 'active'
656
+ ? {
657
+ archive: selectedMessageCanMove
658
+ ? (message) => {
659
+ const archive = foldersByAccountId
660
+ .get(message.accountId)
661
+ ?.find((folder) => folder.type === 'archive');
662
+ if (archive)
663
+ mutateMessage(mail.moveMessage({
664
+ accountId: message.accountId,
665
+ messageId: message.id,
666
+ providerFolderId: archive.providerFolderId,
667
+ }), true);
668
+ }
669
+ : undefined,
670
+ delete: (message) => deleteWorkspaceMessage(message),
671
+ canDelete: (message) => Boolean(!message.scheduledSend &&
672
+ (selectedMessageProviderCapabilities?.moveMessage ||
673
+ isMessageInTrash(message, foldersByAccountId) ||
674
+ message.providerMessageId.startsWith('local-draft:'))),
675
+ downloadAttachment: (message, attachment) => {
676
+ void downloadAttachment(mail, message, attachment).catch((cause) => setError(mailErrorMessage(cause, t('workspace.downloadFailed', {
677
+ defaultValue: 'Unable to download attachment.',
678
+ }))));
679
+ },
680
+ reply: (message) => {
681
+ if (preparingReplyRef.current || composerRequest)
682
+ return;
683
+ preparingReplyRef.current = true;
684
+ void prepareMailReply(mail, message)
685
+ .then((reply) => {
686
+ openComposer(reply.value, reply.attachments, reply.accountId, reply.uploads);
687
+ })
688
+ .catch(requestError)
689
+ .finally(() => {
690
+ preparingReplyRef.current = false;
691
+ });
692
+ },
693
+ forward: (message) => openComposer({
632
694
  ...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({
695
+ mode: 'forward',
696
+ relatedMessageId: message.id,
697
+ subject: forwardSubject(message.subject),
698
+ forwardQuote: createForwardQuote(message),
699
+ forwardBodyIncluded: true,
700
+ }, [], message.accountId),
701
+ cancelScheduled: (message) => editScheduledDraft(message, () => { }),
702
+ editDraft: selectedMessageCanDraft
703
+ ? (message) => editScheduledDraft(message, (message) => openComposer({
704
+ ...EMPTY_COMPOSER,
705
+ mode: 'edit',
706
+ draftMessageId: message.id,
707
+ draftRevision: message.draftRevision,
708
+ draftSource: message.draftSource,
709
+ fromAddress: message.from?.address,
710
+ to: formatAddressList(message.to),
711
+ cc: formatAddressList(message.cc),
712
+ bcc: formatAddressList(message.bcc),
713
+ subject: message.subject,
714
+ ...readDraftComposerBody(message),
715
+ draftConflict: message.draftConflict,
716
+ }, message.attachments, message.accountId))
717
+ : undefined,
718
+ toggleRead: (message) => mutateMessage(setMessageRead(message, !message.read)),
719
+ toggleStarred: (message) => mutateMessage(mail.updateMessage({
720
+ accountId: message.accountId,
721
+ messageId: message.id,
722
+ starred: !message.starred,
723
+ })),
724
+ toggleTodo: (message) => mutateMessage(mail.updateMessage({
664
725
  accountId: message.accountId,
665
726
  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) => {
727
+ todo: !message.todo,
728
+ })),
729
+ saveNote: (message, note) => mutateMessage(mail.updateMessage({
730
+ accountId: message.accountId,
731
+ messageId: message.id,
732
+ note: note.trim() || null,
733
+ })),
734
+ toggleLabel: selectedMessageCanUseLabels
735
+ ? (message, labelId, assigned) => mutateMessage(mail.updateMessageLabels({
736
+ accountId: message.accountId,
737
+ messageId: message.id,
738
+ addLabelIds: assigned ? [labelId] : [],
739
+ removeLabelIds: assigned ? [] : [labelId],
740
+ }))
741
+ : undefined,
742
+ }
743
+ : undefined, actionLabels: {
744
+ archive: t('workspace.archive', { defaultValue: 'Archive' }),
745
+ delete: selectedMessageInTrash
746
+ ? t('workspace.permanentlyDelete', {
747
+ defaultValue: 'Permanently delete',
748
+ })
749
+ : t('workspace.delete', { defaultValue: 'Delete' }),
750
+ download: t('workspace.download', {
751
+ defaultValue: 'Download',
752
+ }),
753
+ reply: t('workspace.reply', { defaultValue: 'Reply' }),
754
+ forward: t('workspace.forward', { defaultValue: 'Forward' }),
755
+ editDraft: t('workspace.editDraft', {
756
+ defaultValue: 'Edit draft',
757
+ }),
758
+ markRead: t('workspace.markRead', {
759
+ defaultValue: 'Mark read',
760
+ }),
761
+ markUnread: t('workspace.markUnread', {
762
+ defaultValue: 'Mark unread',
763
+ }),
764
+ star: t('workspace.star', { defaultValue: 'Star' }),
765
+ unstar: t('workspace.unstar', {
766
+ defaultValue: 'Remove star',
767
+ }),
768
+ collapseMessage: t('workspace.collapseMessage', {
769
+ defaultValue: 'Collapse message',
770
+ }),
771
+ expandMessage: t('workspace.expandMessage', {
772
+ defaultValue: 'Expand message',
773
+ }),
774
+ }, labels: {
775
+ attachmentCount: (count) => t('workspace.attachmentCount', {
776
+ count,
777
+ defaultValue: '{{count}} attachments',
778
+ }),
779
+ conversation: (count) => t('workspace.conversation', {
780
+ count,
781
+ defaultValue: '{{count}} messages in this conversation',
782
+ }),
783
+ loadMore: t('workspace.loadEarlier', {
784
+ defaultValue: 'Load earlier messages',
785
+ }),
786
+ noSubject: t('workspace.noSubject', {
787
+ defaultValue: '(no subject)',
788
+ }),
789
+ selectMessage: t('workspace.selectMessage', {
790
+ defaultValue: 'Select a message to read it.',
791
+ }),
792
+ unknownSender: t('workspace.unknownSender', {
793
+ defaultValue: 'Unknown sender',
794
+ }),
795
+ labels: t('workspace.labels', { defaultValue: 'Labels' }),
796
+ note: t('workspace.note', { defaultValue: 'Note' }),
797
+ notePlaceholder: t('workspace.notePlaceholder', {
798
+ defaultValue: 'Add a private note…',
799
+ }),
800
+ saveNote: t('workspace.saveNote', {
801
+ defaultValue: 'Save note',
802
+ }),
803
+ todo: t('workspace.todo', { defaultValue: 'To do' }),
804
+ more: t('workspace.more', { defaultValue: 'More actions' }),
805
+ }, 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', {
806
+ defaultValue: 'Close mailbox navigation',
807
+ }), finalFocus: () => navigationButtonRef.current?.getClientRects().length
808
+ ? navigationButtonRef.current
809
+ : workspaceRef.current?.querySelector('input'), children: [_jsx(SheetHeader, { className: 'pr-14', children: _jsx(SheetTitle, { children: t('workspace.mailboxNavigation', {
810
+ defaultValue: 'Mailboxes and folders',
811
+ }) }) }), _jsx("div", { className: 'min-h-0 flex-1 overflow-y-auto', onClickCapture: (event) => {
812
+ // Current folders can return early inside the reused sidebar itself.
813
+ if (event.target instanceof Element &&
814
+ event.target.closest('button[aria-current="page"]'))
815
+ closeMailboxNavigation();
816
+ }, children: mailboxNavigation })] }) }), composerRequest ? (_jsx(MailWorkspaceComposer, { onSelectAccount: rememberComposeAccount, request: composerRequest, accounts: accounts, providers: providers, templateVariables: templateVariables, onClose: () => setComposerRequest(undefined), onComplete: (result, rejectedRecipients, operationError) => {
734
817
  setReloadVersion((version) => version + 1);
735
818
  const notice = result === 'partial'
736
819
  ? t('workspace.submissionPartial', {
@@ -813,9 +896,6 @@ function isMessageInTrash(message, foldersByAccountId) {
813
896
  function formatAddressList(addresses) {
814
897
  return addresses.map((address) => address.address).join(', ');
815
898
  }
816
- function replySubject(subject) {
817
- return /^re:/iu.test(subject.trim()) ? subject : `Re: ${subject}`;
818
- }
819
899
  function forwardSubject(subject) {
820
900
  return /^fwd?:/iu.test(subject.trim()) ? subject : `Fwd: ${subject}`;
821
901
  }