@rapidmx/web-client 0.15.1 → 0.17.0
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.
- package/README.md +381 -373
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -480
- package/apps/admin/distribution-lists/new/index.tsx +3 -1
- package/apps/admin/domains/[uid].tsx +166 -164
- package/apps/admin/domains/new/index.tsx +3 -1
- package/apps/admin/escrow-scopes/[uid].tsx +350 -348
- package/apps/admin/escrow-scopes/new/index.tsx +3 -1
- package/apps/admin/index.tsx +129 -124
- package/apps/admin/mailboxes/[uid].tsx +271 -224
- package/apps/admin/mailboxes/new/index.tsx +30 -28
- package/apps/admin/plugins/index.tsx +15 -15
- package/apps/admin/retention-policy/index.tsx +39 -39
- package/apps/admin/signing-certificates/index.tsx +343 -343
- package/apps/admin/transport-rules/new/index.tsx +3 -1
- package/apps/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/escrow/matters/new/index.tsx +3 -1
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
- package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
- package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/EventEditor.tsx +5 -10
- package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
- package/apps/shared/components/calendar/EventModal.tsx +206 -206
- package/apps/shared/components/calendar/GuestInput.tsx +100 -0
- package/apps/shared/components/calendar/MonthView.tsx +185 -185
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
- package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/calendar/eventForm.ts +2 -2
- package/apps/shared/components/calendar/eventFormat.ts +49 -22
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
- package/apps/shared/components/contacts/ContactForm.tsx +383 -383
- package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
- package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +461 -480
- package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
- package/apps/shared/components/layout/UserMenu.tsx +439 -424
- package/apps/shared/components/mail/ConversationList.tsx +323 -323
- package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
- package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
- package/apps/shared/components/mail/MenuButton.tsx +31 -11
- package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
- package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
- package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
- package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
- package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
- package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
- package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
- package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
- package/apps/shared/components/mail/reading/messageExport.ts +59 -0
- package/apps/shared/components/mail/reading/printMessage.ts +99 -0
- package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -125
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
- package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
- package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
- package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
- package/apps/shared/keyboard/dispatch.ts +124 -124
- package/apps/shared/keyboard/format.ts +89 -89
- package/apps/shared/keyboard/keymap.ts +114 -114
- package/apps/shared/keyboard/registry.ts +65 -65
- package/apps/shared/keyboard/targets.ts +79 -79
- package/apps/shared/mail/folderCounts.ts +17 -0
- package/apps/shared/mail/folderOfType.ts +49 -0
- package/apps/shared/mail/folderTree.ts +143 -143
- package/apps/shared/mail/listAllPages.ts +39 -39
- package/apps/shared/mail/newMailNotifications.ts +171 -171
- package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
- package/apps/shared/mail/outbox/sendJob.ts +19 -1
- package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
- package/apps/shared/mail/permanentDelete.ts +160 -0
- package/apps/shared/mail/primaryMailbox.ts +71 -0
- package/apps/shared/mail/reportNotices.ts +66 -0
- package/apps/shared/mail/senderBlocking.ts +141 -0
- package/apps/shared/mail/useCollapsedSections.ts +146 -0
- package/apps/shared/mail/useMailConnection.ts +205 -203
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
- package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
- package/apps/shared/mail/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +178 -178
- package/apps/shared/mail/usePermanentDelete.tsx +160 -0
- package/apps/shared/navigation/frameContext.tsx +1 -1
- package/apps/shared/navigation/index.ts +43 -0
- package/apps/shared/navigation/pageTitle.ts +19 -0
- package/apps/shared/notifications/store.ts +560 -560
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
- package/apps/shared/search/crossMailboxSearch.ts +169 -0
- package/apps/shared/search/localIndexBuilder.ts +481 -481
- package/apps/shared/signing/enrollmentStorage.ts +33 -33
- package/apps/shared/signing/enrollmentTracker.ts +385 -385
- package/apps/shared/signing/enrollmentView.ts +251 -251
- package/apps/shared/signing/useNow.ts +19 -19
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
- package/apps/shared/styles/app.css +396 -386
- package/apps/www/_shell.tsx +82 -0
- package/apps/www/calendar/index.tsx +581 -571
- package/apps/www/contacts/[uid].tsx +112 -109
- package/apps/www/contacts/index.tsx +14 -3
- package/apps/www/index.tsx +2962 -2479
- package/apps/www/messages/[uid].tsx +139 -135
- package/apps/www/settings/appearance/index.tsx +5 -2
- package/apps/www/settings/auto-reply/index.tsx +136 -133
- package/apps/www/settings/blocked-senders/index.tsx +303 -0
- package/apps/www/settings/encryption/index.tsx +1290 -1287
- package/apps/www/settings/filters/[uid].tsx +179 -176
- package/apps/www/settings/filters/index.tsx +105 -102
- package/apps/www/settings/filters/new/index.tsx +165 -140
- package/apps/www/settings/labels/index.tsx +207 -204
- package/apps/www/settings/privacy/index.tsx +495 -492
- package/apps/www/settings/profile/index.tsx +251 -248
- package/apps/www/settings/read-receipts/index.tsx +150 -147
- package/apps/www/settings/sharing/index.tsx +259 -256
- package/apps/www/settings/signatures/[uid].tsx +175 -172
- package/apps/www/settings/signatures/index.tsx +91 -88
- package/apps/www/settings/signatures/new/index.tsx +138 -135
- package/apps/www/tasks/index.tsx +654 -649
- package/dist/apps/admin/data-requests/index.js +1 -1
- package/dist/apps/admin/distribution-lists/new/index.js +3 -1
- package/dist/apps/admin/domains/[uid].js +3 -1
- package/dist/apps/admin/domains/new/index.js +3 -1
- package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
- package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
- package/dist/apps/admin/index.js +5 -2
- package/dist/apps/admin/mailboxes/[uid].js +15 -3
- package/dist/apps/admin/mailboxes/new/index.js +3 -1
- package/dist/apps/admin/transport-rules/new/index.js +3 -1
- package/dist/apps/escrow/matters/new/index.js +3 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
- package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
- package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
- package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
- package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
- package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
- package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
- package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
- package/dist/apps/shared/components/layout/AppShell.js +24 -33
- package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
- package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
- package/dist/apps/shared/components/layout/UserMenu.js +6 -5
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
- package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
- package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
- package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
- package/dist/apps/shared/components/mail/MenuButton.js +15 -11
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
- package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
- package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
- package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
- package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
- package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
- package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
- package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
- package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
- package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
- package/dist/apps/shared/mail/folderCounts.js +12 -0
- package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
- package/dist/apps/shared/mail/folderOfType.js +40 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
- package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
- package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
- package/dist/apps/shared/mail/permanentDelete.js +127 -0
- package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
- package/dist/apps/shared/mail/primaryMailbox.js +61 -0
- package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
- package/dist/apps/shared/mail/reportNotices.js +47 -0
- package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
- package/dist/apps/shared/mail/senderBlocking.js +92 -0
- package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
- package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
- package/dist/apps/shared/mail/useMailConnection.js +3 -1
- package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
- package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
- package/dist/apps/shared/navigation/index.d.ts +16 -0
- package/dist/apps/shared/navigation/index.js +34 -0
- package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
- package/dist/apps/shared/navigation/pageTitle.js +12 -0
- package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
- package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
- package/dist/apps/shared/styles/app.css +396 -386
- package/dist/apps/www/_shell.d.ts +26 -0
- package/dist/apps/www/_shell.js +31 -0
- package/dist/apps/www/calendar/index.d.ts +7 -2
- package/dist/apps/www/calendar/index.js +7 -4
- package/dist/apps/www/contacts/[uid].d.ts +14 -3
- package/dist/apps/www/contacts/[uid].js +5 -3
- package/dist/apps/www/contacts/index.d.ts +7 -2
- package/dist/apps/www/contacts/index.js +8 -4
- package/dist/apps/www/index.d.ts +7 -2
- package/dist/apps/www/index.js +542 -207
- package/dist/apps/www/messages/[uid].d.ts +14 -3
- package/dist/apps/www/messages/[uid].js +7 -5
- package/dist/apps/www/settings/appearance/index.d.ts +8 -2
- package/dist/apps/www/settings/appearance/index.js +4 -2
- package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
- package/dist/apps/www/settings/auto-reply/index.js +4 -2
- package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
- package/dist/apps/www/settings/blocked-senders/index.js +134 -0
- package/dist/apps/www/settings/encryption/index.d.ts +7 -2
- package/dist/apps/www/settings/encryption/index.js +4 -2
- package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
- package/dist/apps/www/settings/filters/[uid].js +4 -2
- package/dist/apps/www/settings/filters/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/index.js +4 -2
- package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/new/index.js +28 -5
- package/dist/apps/www/settings/labels/index.d.ts +7 -2
- package/dist/apps/www/settings/labels/index.js +4 -2
- package/dist/apps/www/settings/privacy/index.d.ts +7 -2
- package/dist/apps/www/settings/privacy/index.js +4 -2
- package/dist/apps/www/settings/profile/index.d.ts +7 -2
- package/dist/apps/www/settings/profile/index.js +4 -2
- package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
- package/dist/apps/www/settings/read-receipts/index.js +4 -2
- package/dist/apps/www/settings/sharing/index.d.ts +7 -2
- package/dist/apps/www/settings/sharing/index.js +4 -2
- package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
- package/dist/apps/www/settings/signatures/[uid].js +4 -2
- package/dist/apps/www/settings/signatures/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/index.js +4 -2
- package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/new/index.js +5 -3
- package/dist/apps/www/tasks/index.d.ts +7 -2
- package/dist/apps/www/tasks/index.js +5 -3
- package/package.json +4 -2
- package/apps/shared/navigation/AppRouter.tsx +0 -300
- package/apps/shared/navigation/idle.ts +0 -45
- package/apps/shared/navigation/routerContext.tsx +0 -83
- package/apps/shared/navigation/routes.ts +0 -72
- package/apps/www/_routedPage.tsx +0 -24
- package/apps/www/_routes.ts +0 -37
- package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
- package/dist/apps/shared/navigation/AppRouter.js +0 -242
- package/dist/apps/shared/navigation/idle.d.ts +0 -14
- package/dist/apps/shared/navigation/idle.js +0 -43
- package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
- package/dist/apps/shared/navigation/routerContext.js +0 -56
- package/dist/apps/shared/navigation/routes.d.ts +0 -32
- package/dist/apps/shared/navigation/routes.js +0 -37
- package/dist/apps/www/_routedPage.d.ts +0 -12
- package/dist/apps/www/_routedPage.js +0 -19
- package/dist/apps/www/_routes.d.ts +0 -11
- package/dist/apps/www/_routes.js +0 -31
|
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
7
|
-
import { HiOutlineArchiveBox, HiOutlineArrowUturnLeft, HiOutlineArrowUturnRight, HiOutlineCheck, HiOutlineExclamationTriangle, HiOutlineFolderArrowDown, HiOutlineLockClosed, HiOutlineMoon, HiOutlineSun, } from "react-icons/hi2";
|
|
7
|
+
import { HiOutlineArchiveBox, HiOutlineArrowUturnLeft, HiOutlineArrowUturnRight, HiOutlineCheck, HiOutlineExclamationTriangle, HiOutlineFolderArrowDown, HiOutlineInboxArrowDown, HiOutlineLockClosed, HiOutlineMoon, HiOutlineNoSymbol, HiOutlineSun, } from "react-icons/hi2";
|
|
8
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
9
|
import { approveReceipt, archiveMessage, attachmentContentUrl, cancelScheduledSend, declineReceipt, getMailbox, getMessage, getMessageRawContent, moveMessage, recallMessage, setMessageLabels, } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
10
10
|
import { buildForwardQuote, buildReplyQuote, buildReplyRecipients, buildReplyThreading, forwardSubject, replySubject, } from "@rapidmx/react-shared/mail/compose/composeQuoting.js";
|
|
@@ -33,6 +33,10 @@ import { useKeyEnvironment } from "../../keyboard/ShortcutProvider.js";
|
|
|
33
33
|
import { useShortcut } from "../../keyboard/useShortcut.js";
|
|
34
34
|
import { useUnlockPrompt } from "../layout/UnlockPromptProvider.js";
|
|
35
35
|
import { moveLocalEntity } from "../../search/localIndexRpcClient.js";
|
|
36
|
+
import { useNavigate } from "../../navigation/index.js";
|
|
37
|
+
import { notify } from "../../notifications/store.js";
|
|
38
|
+
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
39
|
+
import { useMailboxUpdateAccess } from "../../mail/useMailboxUpdateAccess.js";
|
|
36
40
|
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
37
41
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
38
42
|
import LabelMenuButton from "./labelMenu.js";
|
|
@@ -41,6 +45,12 @@ import EncryptedBody from "./reading/EncryptedBody.js";
|
|
|
41
45
|
import { displaySubject } from "./reading/EncryptedPreview.js";
|
|
42
46
|
import MessageBody, { BodySkeleton } from "./reading/MessageBody.js";
|
|
43
47
|
import { BODY_FONT_STYLE, CardShell, SenderAvatar, SubjectCard } from "./reading/MessageCard.js";
|
|
48
|
+
import MessageMoreMenu from "./reading/MessageMoreMenu.js";
|
|
49
|
+
import MessageSourceDialog from "./reading/MessageSourceDialog.js";
|
|
50
|
+
import { useMessageActions } from "./reading/useMessageActions.js";
|
|
51
|
+
import { saveAsEml } from "./reading/messageExport.js";
|
|
52
|
+
import { buildPrintDocument, printDocument } from "./reading/printMessage.js";
|
|
53
|
+
import { fetchBodyContent } from "./reading/bodyContent.js";
|
|
44
54
|
import { useViewOriginal } from "./reading/viewOriginal.js";
|
|
45
55
|
import { ROW_FOCUS_CLASS, UnreadLabel, dateClass, senderClass } from "./unreadStyle.js";
|
|
46
56
|
/** Labels/styling for `specs/end-to-end_encryption.md`'s "Message Security Indicators" table - kept as
|
|
@@ -142,9 +152,11 @@ function downloadMimeAttachment(attachment) {
|
|
|
142
152
|
* A plain `<button>` rather than react-shared's `Button`, whose padding and minimum width are sized for a
|
|
143
153
|
* text label. Ordinary DOM order, so the keyboard reaches these in the order they are read.
|
|
144
154
|
*/
|
|
145
|
-
|
|
155
|
+
/** The look of the card's round icon buttons, shared with the "More actions" trigger. */
|
|
156
|
+
const ICON_BUTTON_CLASS = "inline-flex items-center justify-center p-1.5 rounded-md border border-border text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent";
|
|
157
|
+
function IconAction({ icon, label, onClick, disabled, reason, busy, onPrefetch, shortcut, }) {
|
|
146
158
|
const env = useKeyEnvironment();
|
|
147
|
-
return (_jsx("button", { type: "button", "aria-label": label, title: shortcut ? withHint(label, shortcut, env) : label, "aria-keyshortcuts": shortcut ? ariaKeyShortcuts(shortcut, env) : undefined, "aria-busy": busy || undefined, disabled: disabled, onClick: onClick, onPointerEnter: onPrefetch, onFocus: onPrefetch, className:
|
|
159
|
+
return (_jsx("button", { type: "button", "aria-label": label, title: reason ?? (shortcut ? withHint(label, shortcut, env) : label), "aria-keyshortcuts": shortcut ? ariaKeyShortcuts(shortcut, env) : undefined, "aria-busy": busy || undefined, disabled: disabled, onClick: onClick, onPointerEnter: onPrefetch, onFocus: onPrefetch, className: ICON_BUTTON_CLASS, children: icon }));
|
|
148
160
|
}
|
|
149
161
|
function SecurityIndicator({ security }) {
|
|
150
162
|
const { label, className } = SECURITY_INDICATOR[security.state];
|
|
@@ -170,9 +182,13 @@ const GENERIC_SIGNATURE_FAILURE_MESSAGE = "This message's digital signature coul
|
|
|
170
182
|
/** Fallback when the raw MIME for an encrypted message can't even be fetched/evaluated - an encrypted
|
|
171
183
|
* message must never be mislabeled "Unprotected" just because loading failed. */
|
|
172
184
|
const ENCRYPTED_LOAD_ERROR = "Couldn't load this message's encrypted content.";
|
|
185
|
+
/** A subject without its \`Re:\`/\`Fwd:\` prefixes. */
|
|
186
|
+
function withoutSubjectPrefixes(subject) {
|
|
187
|
+
return (subject ?? "").replace(/^(\s*(re|fw|fwd|aw|sv)\s*:)+/i, "").trim();
|
|
188
|
+
}
|
|
173
189
|
/** A subject without its \`Re:\`/\`Fwd:\` prefixes, lower-cased - what makes two messages of one thread the same subject. */
|
|
174
190
|
function subjectCore(subject) {
|
|
175
|
-
return (subject
|
|
191
|
+
return withoutSubjectPrefixes(subject).toLowerCase();
|
|
176
192
|
}
|
|
177
193
|
/** Whether a message's subject says nothing its thread's subject doesn't. */
|
|
178
194
|
function sameSubject(subject, threadSubject) {
|
|
@@ -209,7 +225,7 @@ export default function MessageDetailPane(props) {
|
|
|
209
225
|
function needsRawSecurityEvaluation(message) {
|
|
210
226
|
return !!message.encrypted || message.hasAttachments;
|
|
211
227
|
}
|
|
212
|
-
function MessageDetailContent({ message: messageProp, attachments, backHref, isSentItems, onRecalled, isOutbox, draftsFolderUid, onScheduledSendCanceled, folders, onMoved, onFolderCreated, onReceiptHandled, onArchived, labels, onLabelsChanged, onLabelCreated, inThread, threadSubject, threadHeader, footer, shortcuts, }) {
|
|
228
|
+
function MessageDetailContent({ message: messageProp, attachments, backHref, isSentItems, onRecalled, isOutbox, draftsFolderUid, onScheduledSendCanceled, folders, onMoved, onFolderCreated, onReceiptHandled, onArchived, onChanged, labels, onLabelsChanged, onLabelCreated, inThread, threadSubject, threadHeader, footer, shortcuts, }) {
|
|
213
229
|
// A copy re-read from the server after an Outbox action was refused (409/403) or a send lease ran out - see
|
|
214
230
|
// `reloadMessage()`. A prop copy newer by `version` wins; an equal-version reload is kept, since claiming a send
|
|
215
231
|
// lease need not bump `version`.
|
|
@@ -678,6 +694,36 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
|
|
|
678
694
|
useShortcut(SHORTCUTS.mail.forward, () => void (!preparingCompose && handleReplyOrForward("forward")), { enabled: keyboard });
|
|
679
695
|
useShortcut(SHORTCUTS.mail.archive, () => void (!archiving && handleArchive()), { enabled: keyboard && archivable });
|
|
680
696
|
useShortcut(SHORTCUTS.mail.move, () => setMovePrompt(true), { enabled: keyboard && movable });
|
|
697
|
+
// The card's Report junk button and "More actions" menu: they act on this message, in the mailbox it is in. Nothing of them applies to a message
|
|
698
|
+
// being written or sent (Drafts, Outbox), and Report junk and Block are not for mail the reader sent.
|
|
699
|
+
const folderType = folders?.find((folder) => folder.uid === message.folderUid)?.type;
|
|
700
|
+
const inJunk = folderType === "junk";
|
|
701
|
+
const inDeletedItems = folderType === "deleted_items";
|
|
702
|
+
const reportable = archivable && !isSentItems;
|
|
703
|
+
const writable = useMailboxUpdateAccess(readerMailbox);
|
|
704
|
+
const remember = (updated) => {
|
|
705
|
+
latestLabelsMessageRef.current = updated;
|
|
706
|
+
setLabelsMessage(updated);
|
|
707
|
+
};
|
|
708
|
+
// Every address the reader owns, in any of their mailboxes: a sender among them is not blocked.
|
|
709
|
+
const readerOwnAddresses = mailboxes.flatMap((mb) => [mb.primarySmtpAddress, ...(mb.aliasAddresses ?? [])]).filter((address) => !!address).map((address) => address.toLowerCase());
|
|
710
|
+
const messageActions = useMessageActions({
|
|
711
|
+
message,
|
|
712
|
+
ownAddresses: readerOwnAddresses,
|
|
713
|
+
newest: () => (message.version >= latestLabelsMessageRef.current.version ? message : latestLabelsMessageRef.current),
|
|
714
|
+
remember,
|
|
715
|
+
folders,
|
|
716
|
+
inJunk,
|
|
717
|
+
inDeletedItems,
|
|
718
|
+
trackMessageChange,
|
|
719
|
+
onMoved,
|
|
720
|
+
onChanged,
|
|
721
|
+
onFolderCreated,
|
|
722
|
+
});
|
|
723
|
+
const navigate = useNavigate();
|
|
724
|
+
const [sourceView, setSourceView] = useState(null);
|
|
725
|
+
// Which "View source" request the dialog is waiting for: one answering after the dialog was closed (or another was opened) is not shown.
|
|
726
|
+
const sourceRequestRef = useRef(0);
|
|
681
727
|
// Only ever invoked from the pending-receipt banner below, which itself only renders once `message`
|
|
682
728
|
// is loaded — same real invariant as every other handler above.
|
|
683
729
|
async function handleReceipt(type, action) {
|
|
@@ -758,13 +804,107 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
|
|
|
758
804
|
const senderRecipient = { displayName: senderName, address: senderAddress };
|
|
759
805
|
const sendingStatus = (_jsxs(_Fragment, { children: [sendInProgress && (_jsx("span", { className: "text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt", children: "Sending\u2026" })), isSentItems &&
|
|
760
806
|
(message.recallRequestedAt ? (_jsx("span", { className: "text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt", children: "Recall requested" })) : (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => setConfirming(true), children: "Recall this message" }))), inOutbox && !sendInProgress && message.scheduledSendTime && (_jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsxs("span", { className: "text-xs font-medium text-text-muted py-1 px-2.5 rounded-pill bg-surface-alt", children: ["Scheduled for ", new Date(message.scheduledSendTime).toLocaleString()] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: canceling, disabled: canceling, onClick: handleCancelScheduledSend, children: "Cancel" })] })), inOutbox && !sendInProgress && !message.scheduledSendTime && draftsFolderUid && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", loading: canceling, disabled: canceling, onClick: handleCancelScheduledSend, children: "Move to Drafts" }))] }));
|
|
807
|
+
// ---- The "More actions" menu's own rows (Print, View, Save as, Create rule); the rest are `messageActions`. ----
|
|
808
|
+
/** Prints this one message: its header lines and the body the pane shows (what was decrypted or verified here, else the server's sanitized
|
|
809
|
+
* body), in a frame of its own - see `printMessage.ts`. */
|
|
810
|
+
async function handlePrint() {
|
|
811
|
+
try {
|
|
812
|
+
const content = bodyContent ?? (await fetchBodyContent(message.uid, message.version));
|
|
813
|
+
const format = (list) => list.map((r) => formatMailAddress(r)).join(", ");
|
|
814
|
+
const printable = buildPrintDocument({
|
|
815
|
+
subject: shownSubject,
|
|
816
|
+
headers: [
|
|
817
|
+
{ name: "From", value: senderLabel },
|
|
818
|
+
{ name: "To", value: format(message.recipients.filter((r) => r.type !== "cc" && r.type !== "bcc")) },
|
|
819
|
+
{ name: "Cc", value: format(message.recipients.filter((r) => r.type === "cc")) },
|
|
820
|
+
{ name: "Date", value: new Date(message.receivedDate).toLocaleString() },
|
|
821
|
+
],
|
|
822
|
+
content,
|
|
823
|
+
attachments,
|
|
824
|
+
inlineParts: innerAttachments,
|
|
825
|
+
});
|
|
826
|
+
if (printable === undefined) {
|
|
827
|
+
notify({ kind: "warning", title: "This message is too large to print here" });
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
printDocument(printable);
|
|
831
|
+
}
|
|
832
|
+
catch (err) {
|
|
833
|
+
notifyApiError(err, "Couldn't print this message");
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/** Opens the source (or just the headers) of the message as the server has it - for an encrypted message, its ciphertext. */
|
|
837
|
+
function openSource(mode) {
|
|
838
|
+
const request = ++sourceRequestRef.current;
|
|
839
|
+
setSourceView({ mode, source: { status: "loading" } });
|
|
840
|
+
getMessageRawContent(message.uid).then((raw) => {
|
|
841
|
+
if (sourceRequestRef.current === request) {
|
|
842
|
+
setSourceView({ mode, source: { status: "ready", raw } });
|
|
843
|
+
}
|
|
844
|
+
}, (err) => {
|
|
845
|
+
if (sourceRequestRef.current === request) {
|
|
846
|
+
setSourceView({ mode, source: { status: "error", message: err instanceof ApiRequestError ? err.message : "Could not load this message's source." } });
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
function closeSource() {
|
|
851
|
+
sourceRequestRef.current++;
|
|
852
|
+
setSourceView(null);
|
|
853
|
+
}
|
|
854
|
+
async function handleSaveAsEml() {
|
|
855
|
+
try {
|
|
856
|
+
saveAsEml(await getMessageRawContent(message.uid), protectedSubject ?? displaySubject(message.subject));
|
|
857
|
+
}
|
|
858
|
+
catch (err) {
|
|
859
|
+
notifyApiError(err, "Couldn't save this message");
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
/** The rules page's "New mail filter", started from this message: its sender and (for a message the server can read) its subject. */
|
|
863
|
+
function handleCreateRule() {
|
|
864
|
+
const params = new URLSearchParams({ mailboxUid: message.mailboxUid, from: message.from.address });
|
|
865
|
+
const subject = message.encrypted ? "" : withoutSubjectPrefixes(message.subject);
|
|
866
|
+
if (subject) {
|
|
867
|
+
params.set("subject", subject);
|
|
868
|
+
}
|
|
869
|
+
navigate(`/settings/filters/new?${params.toString()}`);
|
|
870
|
+
}
|
|
871
|
+
const menuActions = {
|
|
872
|
+
replyAll: () => void handleReplyOrForward("replyAll"),
|
|
873
|
+
forward: () => void handleReplyOrForward("forward"),
|
|
874
|
+
deleteMessage: () => void messageActions.deleteMessage(),
|
|
875
|
+
toggleRead: () => void messageActions.toggleRead(),
|
|
876
|
+
toggleFlag: () => void messageActions.toggleFlag(),
|
|
877
|
+
reportJunk: () => void messageActions.reportJunk(),
|
|
878
|
+
reportPhishing: () => void messageActions.reportPhishing(),
|
|
879
|
+
notJunk: () => void messageActions.notJunk(),
|
|
880
|
+
notJunkAndTrust: () => void messageActions.notJunkAndTrust(),
|
|
881
|
+
blockSender: () => void messageActions.blockSender(),
|
|
882
|
+
neverBlockSender: () => void messageActions.neverBlockSender(),
|
|
883
|
+
print: () => void handlePrint(),
|
|
884
|
+
viewSource: () => openSource("source"),
|
|
885
|
+
viewDetails: () => openSource("headers"),
|
|
886
|
+
saveAsEml: () => void handleSaveAsEml(),
|
|
887
|
+
createRule: handleCreateRule,
|
|
888
|
+
};
|
|
889
|
+
// An encrypted message can be printed once it has been opened here: while it is still opening, or when it can't be, there is nothing to print.
|
|
890
|
+
const printReason = !message.encrypted
|
|
891
|
+
? undefined
|
|
892
|
+
: security === null
|
|
893
|
+
? "Still opening this message"
|
|
894
|
+
: security.decryptError === undefined
|
|
895
|
+
? undefined
|
|
896
|
+
: lockedRef.current
|
|
897
|
+
? "Unlock this message to print it"
|
|
898
|
+
: "This message can't be read, so it can't be printed";
|
|
899
|
+
// In Junk Email the card's button is the way back (Not junk), which only needs the right to change the message.
|
|
900
|
+
const reportReason = !writable ? "View-only mailbox" : undefined;
|
|
761
901
|
const card = (_jsxs(CardShell, { unread: cardUnread, children: [_jsxs("div", { className: ["flex flex-wrap items-start gap-x-3 gap-y-1 px-4 pt-3 pb-2", cardUnread ? "bg-primary/[0.07]" : ""].join(" "), children: [_jsx(SenderAvatar, { from: senderRecipient }), _jsxs("div", { className: "flex-1 min-w-[12rem]", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: [threadHeader ? (_jsx("h2", { className: "min-w-0 flex-1", style: BODY_FONT_STYLE, children: _jsxs("button", { type: "button", ref: threadHeader.buttonRef, onClick: threadHeader.onToggle, "aria-expanded": true, "aria-controls": threadHeader.bodyId, className: [
|
|
762
902
|
"w-full text-left rounded-sm text-sm",
|
|
763
903
|
ROW_FOCUS_CLASS,
|
|
764
904
|
].join(" "), children: [_jsx(UnreadLabel, { unread: cardUnread }), _jsx(MailAddress, { recipient: senderRecipient, className: senderClass(cardUnread) })] }) })) : (_jsxs("p", { className: "text-sm break-words min-w-0", children: ["From ", _jsx("span", { className: "font-semibold text-text", children: senderLabel })] })), security && _jsx(SecurityIndicator, { security: security })] }), senderNameCheck.misleading && (_jsxs("p", { role: "status", className: "mt-2 py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: ["The sender\u2019s name \u201C", senderName, "\u201D looks like an email address, but this message was sent from ", _jsx("span", { className: "font-medium", children: senderAddress }), ". Don\u2019t trust it based on the name."] })), _jsx(RecipientLine, { label: "To", recipients: message.recipients.filter((r) => r.type !== "cc" && r.type !== "bcc") }), _jsx(RecipientLine, { label: "Cc", recipients: message.recipients.filter((r) => r.type === "cc") }), _jsx(RecipientLine, { label: "Bcc", recipients: message.recipients.filter((r) => r.type === "bcc") })] }), _jsxs("div", { className: "ml-auto flex flex-wrap items-center justify-end gap-1", children: [_jsx("time", { dateTime: message.receivedDate, className: ["text-xs mr-1", dateClass(cardUnread)].join(" "), children: new Date(message.receivedDate).toLocaleString() }), adaptable && (_jsx("button", { type: "button", "aria-label": "View original", "aria-pressed": viewOriginal, title: viewOriginal ? "Follow the theme" : "View original", onClick: () => setViewOriginal(!viewOriginal), className: [
|
|
765
905
|
"inline-flex items-center justify-center p-1.5 rounded-md border border-border text-sm text-text hover:bg-surface-alt",
|
|
766
906
|
viewOriginal ? "bg-primary/10" : "",
|
|
767
|
-
].join(" "), children: viewOriginal ? _jsx(HiOutlineMoon, { size: 16, "aria-hidden": "true" }) : _jsx(HiOutlineSun, { size: 16, "aria-hidden": "true" }) })), _jsx(IconAction, { icon: _jsx(HiOutlineArrowUturnLeft, { size: 16, "aria-hidden": "true" }), label: "Reply", shortcut: keyboard ? SHORTCUTS.mail.reply : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("reply") }), _jsx(IconAction, { icon: _jsxs("span", { className: "inline-flex items-center", "aria-hidden": "true", children: [_jsx(HiOutlineArrowUturnLeft, { size: 16 }), _jsx(HiOutlineArrowUturnLeft, { size: 16, className: "-ml-2.5" })] }), label: "Reply All", shortcut: keyboard ? SHORTCUTS.mail.replyAll : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("replyAll") }), _jsx(IconAction, { icon: _jsx(HiOutlineArrowUturnRight, { size: 16, "aria-hidden": "true" }), label: "Forward", shortcut: keyboard ? SHORTCUTS.mail.forward : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("forward") }), archivable && (_jsx(IconAction, { icon: _jsx(HiOutlineArchiveBox, { size: 16, "aria-hidden": "true" }), label: "Archive", shortcut: keyboard ? SHORTCUTS.mail.archive : undefined, busy: archiving, disabled: archiving, onClick: handleArchive })), movable && (_jsx(IconAction, { icon: _jsx(HiOutlineFolderArrowDown, { size: 16, "aria-hidden": "true" }), label: "Move to", shortcut: keyboard ? SHORTCUTS.mail.move : undefined, onClick: () => setMovePrompt(true) })), labels && labels.length > 0 && (_jsx(LabelMenuButton, { "aria-label": "Labels", label: "Labels", className: "border border-border py-1.5", labels: labels, mailboxUid: message.mailboxUid, onLabelCreated: onLabelCreated, applied: currentLabelsMessage.labelUids ?? [], onCommit: (labelUids) => void handleApplyLabels(labelUids), busy: savingLabels, note: "Ticked labels are applied to this message and unticked ones removed.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }))] })] }), (answered || forwarded) && (_jsx("p", { className: "px-4 py-1.5 text-xs text-text-muted bg-surface-alt border-y border-border", children: answered && forwarded ? "You replied to and forwarded this message." : answered ? "You replied to this message." : "You forwarded this message." })), _jsxs("div", { id: threadHeader?.bodyId, className: "px-4 pb-3 flex flex-col gap-2", children: [inThread && (subjectDiffers || !sameSubject(shownSubject, threadSubject)) && (_jsx("h3", { className: "text-sm font-semibold break-words", children: shownSubject })), _jsx("div", { className: "flex flex-wrap items-center gap-2 empty:hidden", children: sendingStatus }), inOutbox && message.scheduledSendError && _jsxs(Alert, { children: ["This message wasn\u2019t sent: ", message.scheduledSendError] }), cancelError && _jsx(Alert, { children: cancelError }), keyChangeNotice && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: keyChangeNotice })), security && UNVERIFIED_SIGNER_STATES.has(security.state) && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: ["Signed, but the signer isn\u2019t a trusted contact key, so the sender isn\u2019t verified.", security.signerEmails && security.signerEmails.length > 0 && _jsxs(_Fragment, { children: [" Certificate for ", security.signerEmails.join(", "), "."] }), security.signerFingerprint && (_jsxs(_Fragment, { children: [" ", "Fingerprint ", _jsx("span", { className: "font-mono text-xs break-all", children: security.signerFingerprint }), "."] }))] })), pendingConflict && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: ["This sender has a signing key change waiting for your review, so this signer can\u2019t be trusted from here.", " ", _jsx("a", { href: `/contacts/${encodeURIComponent(senderKeyState.contactUid)}`, className: "font-medium text-primary-dark hover:underline", children: "Review it in Contacts" })] })), trustableCertificate && (_jsx("div", { children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => {
|
|
907
|
+
].join(" "), children: viewOriginal ? _jsx(HiOutlineMoon, { size: 16, "aria-hidden": "true" }) : _jsx(HiOutlineSun, { size: 16, "aria-hidden": "true" }) })), _jsx(IconAction, { icon: _jsx(HiOutlineArrowUturnLeft, { size: 16, "aria-hidden": "true" }), label: "Reply", shortcut: keyboard ? SHORTCUTS.mail.reply : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("reply") }), _jsx(IconAction, { icon: _jsxs("span", { className: "inline-flex items-center", "aria-hidden": "true", children: [_jsx(HiOutlineArrowUturnLeft, { size: 16 }), _jsx(HiOutlineArrowUturnLeft, { size: 16, className: "-ml-2.5" })] }), label: "Reply All", shortcut: keyboard ? SHORTCUTS.mail.replyAll : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("replyAll") }), _jsx(IconAction, { icon: _jsx(HiOutlineArrowUturnRight, { size: 16, "aria-hidden": "true" }), label: "Forward", shortcut: keyboard ? SHORTCUTS.mail.forward : undefined, disabled: preparingCompose, onPrefetch: prefetchReply, onClick: () => void handleReplyOrForward("forward") }), archivable && (_jsx(IconAction, { icon: _jsx(HiOutlineArchiveBox, { size: 16, "aria-hidden": "true" }), label: "Archive", shortcut: keyboard ? SHORTCUTS.mail.archive : undefined, busy: archiving, disabled: archiving, onClick: handleArchive })), movable && (_jsx(IconAction, { icon: _jsx(HiOutlineFolderArrowDown, { size: 16, "aria-hidden": "true" }), label: "Move to", shortcut: keyboard ? SHORTCUTS.mail.move : undefined, onClick: () => setMovePrompt(true) })), reportable && (_jsx(IconAction, { icon: inJunk ? _jsx(HiOutlineInboxArrowDown, { size: 16, "aria-hidden": "true" }) : _jsx(HiOutlineNoSymbol, { size: 16, "aria-hidden": "true" }), label: inJunk ? "Not junk" : "Report junk", busy: messageActions.busy, disabled: messageActions.busy || reportReason !== undefined, reason: reportReason, onClick: () => void (inJunk ? messageActions.notJunk() : messageActions.reportJunk()) })), labels && labels.length > 0 && (_jsx(LabelMenuButton, { "aria-label": "Labels", label: "Labels", className: "border border-border py-1.5", labels: labels, mailboxUid: message.mailboxUid, onLabelCreated: onLabelCreated, applied: currentLabelsMessage.labelUids ?? [], onCommit: (labelUids) => void handleApplyLabels(labelUids), busy: savingLabels, note: "Ticked labels are applied to this message and unticked ones removed.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } })), archivable && (_jsx(MessageMoreMenu, { actions: menuActions, senderAddress: message.from.address, read: currentLabelsMessage.flags.read === true, flagged: currentLabelsMessage.flags.flagged === true, writable: writable, busy: messageActions.busy, inJunk: inJunk, inDeletedItems: inDeletedItems, sent: !!isSentItems, ownSender: readerOwnAddresses.includes(message.from.address.toLowerCase()), canTrustSender: writable, printReason: printReason, composing: preparingCompose, triggerClassName: ICON_BUTTON_CLASS }))] })] }), (answered || forwarded) && (_jsx("p", { className: "px-4 py-1.5 text-xs text-text-muted bg-surface-alt border-y border-border", children: answered && forwarded ? "You replied to and forwarded this message." : answered ? "You replied to this message." : "You forwarded this message." })), _jsxs("div", { id: threadHeader?.bodyId, className: "px-4 pb-3 flex flex-col gap-2", children: [inThread && (subjectDiffers || !sameSubject(shownSubject, threadSubject)) && (_jsx("h3", { className: "text-sm font-semibold break-words", children: shownSubject })), _jsx("div", { className: "flex flex-wrap items-center gap-2 empty:hidden", children: sendingStatus }), inOutbox && message.scheduledSendError && _jsxs(Alert, { children: ["This message wasn\u2019t sent: ", message.scheduledSendError] }), cancelError && _jsx(Alert, { children: cancelError }), keyChangeNotice && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: keyChangeNotice })), security && UNVERIFIED_SIGNER_STATES.has(security.state) && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: ["Signed, but the signer isn\u2019t a trusted contact key, so the sender isn\u2019t verified.", security.signerEmails && security.signerEmails.length > 0 && _jsxs(_Fragment, { children: [" Certificate for ", security.signerEmails.join(", "), "."] }), security.signerFingerprint && (_jsxs(_Fragment, { children: [" ", "Fingerprint ", _jsx("span", { className: "font-mono text-xs break-all", children: security.signerFingerprint }), "."] }))] })), pendingConflict && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: ["This sender has a signing key change waiting for your review, so this signer can\u2019t be trusted from here.", " ", _jsx("a", { href: `/contacts/${encodeURIComponent(senderKeyState.contactUid)}`, className: "font-medium text-primary-dark hover:underline", children: "Review it in Contacts" })] })), trustableCertificate && (_jsx("div", { children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => {
|
|
768
908
|
setTrustError(null);
|
|
769
909
|
setTrustConfirmOpen(true);
|
|
770
910
|
}, children: "Trust this signer" }) })), subjectDiffers && (_jsxs("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-surface-alt text-text", children: ["The subject shown above is the one the sender signed. It differs from the subject this message was delivered with (\u201C", message.subject, "\u201D), which may have been changed on the way, e.g. by a mailing list."] })), security?.headerTamperDetected && (_jsx(Alert, { children: "This message's visible From/To/Cc/Date/Subject don't match what the sender actually signed or encrypted \u2014 an intermediary may have altered them after sending. Treat the fields shown above with caution." })), security?.state === "verified_at_first_open" && (_jsx("p", { role: "status", className: `py-2 px-3 rounded-sm text-sm text-text ${security.laterCompromised ? "bg-warning/15" : "bg-surface-alt"}`, children: verifiedAtFirstOpenMessage(security) })), keyChanged && (_jsxs("section", { "aria-label": "Signing key changed", className: "py-3 px-3 rounded-sm text-sm bg-warning/15 text-text flex flex-col gap-2", children: [_jsx("h2", { className: "font-semibold", children: "This sender\u2019s signing key changed" }), _jsxs("p", { children: ["The signature on this message is valid and its certificate names ", senderAddress, ", but it was made with a different key than the one you trust for this sender", keyChanged.state === "verified_at_first_open" ? "." : _jsx(_Fragment, { children: ", so it isn\u2019t verified." })] }), _jsx(KeyChangeReview, { mailboxUid: message.mailboxUid, address: senderAddress, useType: "sign", ownerName: "the sender", current: pinnedSignerKey && { fingerprint: pinnedSignerKey.fingerprint, since: senderKeyState.pinnedSince }, proposed: {
|
|
@@ -782,5 +922,5 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
|
|
|
782
922
|
.map((type) => (_jsxs("div", { className: "flex flex-wrap items-center gap-2 py-2 px-3 rounded-sm bg-surface-alt text-sm", children: [_jsxs("span", { children: [senderLabel, " requested a ", type, " receipt for this message."] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: receiptBusy === type, disabled: receiptBusy !== null, onClick: () => handleReceipt(type, "approve"), children: "Send receipt" }), _jsx(Button, { type: "button", variant: "text", disabled: receiptBusy !== null, onClick: () => handleReceipt(type, "decline"), children: "Decline" })] }, type))), receiptError && _jsx(Alert, { children: receiptError }), mayHoldInvite && _jsx(InviteCard, { messageUid: message.uid, headingLevel: inThread ? 3 : 2 }), security?.state === "signature_failed" && innerAttachments && innerAttachments.length > 0 && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: "These attachments come from a message whose signature couldn\u2019t be verified. Open them only if you trust the sender." })), innerAttachments
|
|
783
923
|
? innerAttachments.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-2", children: innerAttachments.map((attachment, index) => (_jsx("li", { children: _jsx("button", { type: "button", onClick: () => downloadMimeAttachment(attachment), className: "text-xs font-medium py-1 px-2.5 rounded-pill bg-surface-alt text-text-muted hover:text-primary-dark", children: attachment.filename ?? `Unnamed ${attachment.contentType} attachment` }) }, `${index}:${attachment.filename ?? ""}`))) }))
|
|
784
924
|
: listedAttachments.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-2", children: listedAttachments.map((attachment) => (_jsx("li", { children: _jsxs("a", { href: attachmentContentUrl(attachment.uid), className: "text-xs font-medium py-1 px-2.5 rounded-pill bg-surface-alt text-text-muted hover:text-primary-dark", children: [attachment.filename, " (", formatBytes(attachment.sizeBytes), ")"] }) }, attachment.uid))) })), _jsx("div", { ref: bodyRegionRef, tabIndex: -1, className: "outline-none", children: message.encrypted && security === null ? (_jsx(BodySkeleton, {})) : security?.decryptError !== undefined ? (_jsx(EncryptedBody, { locked: lockedRef.current, reason: security.decryptError, unlocking: unlocking, onUnlock: () => void handleUnlockToView() })) : (_jsx(MessageBody, { messageUid: message.uid, messageVersion: message.version, title: bodyTitle, content: bodyContent, attachments: attachments, inlineParts: innerAttachments, original: viewOriginal, onAdaptable: setAdaptable })) })] }), showFooter && (_jsxs("div", { className: "flex flex-wrap gap-2 px-4 py-3 border-t border-border print:hidden", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Reply to this message", disabled: preparingCompose, onClick: () => void handleReplyOrForward("reply"), children: "Reply" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Reply all to this message", disabled: preparingCompose, onClick: () => void handleReplyOrForward("replyAll"), children: "Reply All" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Forward this message", disabled: preparingCompose, onClick: () => void handleReplyOrForward("forward"), children: "Forward" })] }))] }));
|
|
785
|
-
return (_jsxs("div", { className: inThread ? "min-w-0" : "flex-1 min-w-0 min-h-0 flex flex-col h-full", children: [inThread ? (card) : (_jsxs(_Fragment, { children: [backHref && (_jsx("a", { href: backHref, className: "text-sm text-primary-dark hover:underline block px-4 pt-3", children: "\u2190 Back to messages" })), _jsx(SubjectCard, { subject: shownSubject }), _jsx("div", { className: "flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3", children: card })] })), _jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: message.mailboxUid, folders: folders ?? [], currentFolderUid: message.folderUid, onMove: handleMove, onFolderCreated: onFolderCreated }), _jsxs(Modal, { open: confirming, onClose: () => setConfirming(false), title: "Recall this message?", children: [_jsx("p", { className: "text-sm text-text-muted mb-4", children: "This asks every original recipient's mail system to delete their copy, but only if it's still unread there \u2014 there's no way to guarantee it, and no confirmation once it either succeeds or fails. Recipients who already read the message will keep it." }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", loading: recalling, disabled: recalling, onClick: handleRecall, className: "!w-auto", children: "Recall message" }), _jsx(Button, { type: "button", variant: "secondary", disabled: recalling, onClick: () => setConfirming(false), className: "!w-auto", children: "Cancel" })] })] }), _jsx(Modal, { open: trustConfirmOpen && !!trustableCertificate, onClose: () => !trusting && setTrustConfirmOpen(false), title: "Trust this signer?", children: _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsxs("p", { children: ["Mail from ", _jsx("span", { className: "font-medium", children: senderAddress }), " signed with this certificate will show as verified."] }), _jsxs("dl", { className: "flex flex-col gap-1", children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate issued to" }), _jsx("dd", { children: security?.signerEmails?.length ? security.signerEmails.join(", ") : "No email address" }), _jsx("dt", { className: "text-xs text-text-muted", children: "Fingerprint" }), _jsx("dd", { className: "font-mono text-xs break-all", children: security?.signerFingerprint ? formatFingerprint(security.signerFingerprint) : "Unknown" })] }), _jsx("p", { className: "text-text-muted", children: "Anyone can create a certificate naming any address. Before trusting it, confirm this fingerprint with the sender through another channel, such as a phone call." }), trustError && _jsx(Alert, { children: trustError }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", className: "!w-auto", loading: trusting, disabled: trusting, onClick: () => handleTrustSigner(senderAddress, trustableCertificate), children: "Trust" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => setTrustConfirmOpen(false), children: "Cancel" })] })] }) })] }));
|
|
925
|
+
return (_jsxs("div", { className: inThread ? "min-w-0" : "flex-1 min-w-0 min-h-0 flex flex-col h-full", children: [inThread ? (card) : (_jsxs(_Fragment, { children: [backHref && (_jsx("a", { href: backHref, className: "text-sm text-primary-dark hover:underline block px-4 pt-3", children: "\u2190 Back to messages" })), _jsx(SubjectCard, { subject: shownSubject }), _jsx("div", { className: "flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3", children: card })] })), _jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: message.mailboxUid, folders: folders ?? [], currentFolderUid: message.folderUid, onMove: handleMove, onFolderCreated: onFolderCreated }), messageActions.dialog, _jsx(MessageSourceDialog, { open: sourceView !== null, onClose: closeSource, mode: sourceView?.mode ?? "source", source: sourceView?.source ?? { status: "loading" }, encrypted: !!message.encrypted }), _jsxs(Modal, { open: confirming, onClose: () => setConfirming(false), title: "Recall this message?", children: [_jsx("p", { className: "text-sm text-text-muted mb-4", children: "This asks every original recipient's mail system to delete their copy, but only if it's still unread there \u2014 there's no way to guarantee it, and no confirmation once it either succeeds or fails. Recipients who already read the message will keep it." }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", loading: recalling, disabled: recalling, onClick: handleRecall, className: "!w-auto", children: "Recall message" }), _jsx(Button, { type: "button", variant: "secondary", disabled: recalling, onClick: () => setConfirming(false), className: "!w-auto", children: "Cancel" })] })] }), _jsx(Modal, { open: trustConfirmOpen && !!trustableCertificate, onClose: () => !trusting && setTrustConfirmOpen(false), title: "Trust this signer?", children: _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsxs("p", { children: ["Mail from ", _jsx("span", { className: "font-medium", children: senderAddress }), " signed with this certificate will show as verified."] }), _jsxs("dl", { className: "flex flex-col gap-1", children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate issued to" }), _jsx("dd", { children: security?.signerEmails?.length ? security.signerEmails.join(", ") : "No email address" }), _jsx("dt", { className: "text-xs text-text-muted", children: "Fingerprint" }), _jsx("dd", { className: "font-mono text-xs break-all", children: security?.signerFingerprint ? formatFingerprint(security.signerFingerprint) : "Unknown" })] }), _jsx("p", { className: "text-text-muted", children: "Anyone can create a certificate naming any address. Before trusting it, confirm this fingerprint with the sender through another channel, such as a phone call." }), trustError && _jsx(Alert, { children: trustError }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", className: "!w-auto", loading: trusting, disabled: trusting, onClick: () => handleTrustSigner(senderAddress, trustableCertificate), children: "Trust" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => setTrustConfirmOpen(false), children: "Cancel" })] })] }) })] }));
|
|
786
926
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface PermanentDeleteDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
title: string;
|
|
5
|
+
/** The question, ending in what cannot be undone. */
|
|
6
|
+
message: string;
|
|
7
|
+
/** The destructive button's own words ("Delete permanently"). */
|
|
8
|
+
confirmLabel: string;
|
|
9
|
+
/** The delete is on the wire: the dialog stays up, and can be neither confirmed again nor dismissed. */
|
|
10
|
+
busy: boolean;
|
|
11
|
+
onConfirm: () => void;
|
|
12
|
+
onCancel: () => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The confirmation in front of every permanent delete. A `Modal`, so Escape and the Cancel button dismiss it and Tab stays inside it. Focus
|
|
16
|
+
* starts on the dialog itself, never on the destructive button, so pressing Enter (or the Delete key again) on a dialog the reader did not
|
|
17
|
+
* mean to open keeps their mail. It remembers nothing: every permanent delete asks.
|
|
18
|
+
*/
|
|
19
|
+
export default function PermanentDeleteDialog({ open, title, message, confirmLabel, busy, onConfirm, onCancel }: PermanentDeleteDialogProps): React.JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
3
|
+
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
4
|
+
/**
|
|
5
|
+
* The confirmation in front of every permanent delete. A `Modal`, so Escape and the Cancel button dismiss it and Tab stays inside it. Focus
|
|
6
|
+
* starts on the dialog itself, never on the destructive button, so pressing Enter (or the Delete key again) on a dialog the reader did not
|
|
7
|
+
* mean to open keeps their mail. It remembers nothing: every permanent delete asks.
|
|
8
|
+
*/
|
|
9
|
+
export default function PermanentDeleteDialog({ open, title, message, confirmLabel, busy, onConfirm, onCancel }) {
|
|
10
|
+
return (_jsxs(Modal, { open: open, onClose: () => !busy && onCancel(), title: title, children: [_jsx("p", { className: "text-sm mb-5", children: message }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: onCancel, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: busy, disabled: busy, onClick: onConfirm, children: confirmLabel })] })] }));
|
|
11
|
+
}
|
|
@@ -10,6 +10,7 @@ import { assembleDraft, attachmentContentUrl, createDraft, deleteMessage, getMai
|
|
|
10
10
|
import { listMailSignatures } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
|
|
11
11
|
import { buildComposeBodyHtml } from "@rapidmx/react-shared/mail/compose/composeQuoting.js";
|
|
12
12
|
import { peekMailboxWritability, useMailboxWritability } from "../writableMailboxes.js";
|
|
13
|
+
import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
13
14
|
import { resolveRecipientEncryption } from "@rapidmx/react-shared/crypto/composeSecurity.js";
|
|
14
15
|
import { getUnlockedKeys, subscribeKeySession } from "@rapidmx/react-shared/crypto/keySession.js";
|
|
15
16
|
import { findActivePublicKey, getEncryptionPolicy, lookupKeys } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
|
|
@@ -219,11 +220,13 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, user
|
|
|
219
220
|
if (cancelled) {
|
|
220
221
|
return;
|
|
221
222
|
}
|
|
222
|
-
|
|
223
|
+
// Listed with the caller's own (primary) mailbox first, and defaulted to below.
|
|
224
|
+
const ordered = orderMailboxes(result, userUid);
|
|
225
|
+
setMailboxes(ordered);
|
|
223
226
|
// Never default to a mailbox already known to be view-only (see the effect below for one
|
|
224
227
|
// that turns out to be).
|
|
225
|
-
const candidates =
|
|
226
|
-
setFromMailboxUid((current) => current ?? (candidates
|
|
228
|
+
const candidates = ordered.filter((mb) => peekMailboxWritability(mb, userUid, trusted) !== false);
|
|
229
|
+
setFromMailboxUid((current) => current ?? primaryMailboxUid(candidates, userUid));
|
|
227
230
|
})
|
|
228
231
|
.catch((err) => {
|
|
229
232
|
// Only fatal when there's no mailbox to fall back on - a session that already names one just
|
|
@@ -481,7 +484,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, user
|
|
|
481
484
|
if (!fromIsViewOnly || hasUploads) {
|
|
482
485
|
return;
|
|
483
486
|
}
|
|
484
|
-
const ordered =
|
|
487
|
+
const ordered = orderMailboxes(mailboxes, userUid);
|
|
485
488
|
const alternative = ordered.find((mb) => writability[mb.uid] === true) ?? ordered.find((mb) => writability[mb.uid] === undefined);
|
|
486
489
|
if (alternative) {
|
|
487
490
|
handleFromChange(alternative.uid);
|
|
@@ -685,6 +688,7 @@ export default function ComposeWindow({ session, onClose, onToggleMinimize, user
|
|
|
685
688
|
encryptRequested: current.encryptRequested,
|
|
686
689
|
forcePlaintext,
|
|
687
690
|
saved: saveInFlightRef.current,
|
|
691
|
+
threading: session.threading,
|
|
688
692
|
};
|
|
689
693
|
if (request.to.length === 0) {
|
|
690
694
|
setSendError("At least one recipient is required.");
|
|
@@ -22,6 +22,26 @@ export interface RecipientInputProps {
|
|
|
22
22
|
disabled?: boolean;
|
|
23
23
|
/** Focuses the input when it mounts - a new message starts in To. */
|
|
24
24
|
autoFocus?: boolean;
|
|
25
|
+
/** Called whenever the field's entries change: every committed entry (chips - a picked suggestion, a finished or
|
|
26
|
+
* pasted one, or a typed one committed by Enter or leaving the field) and the text still being typed (`""` after a
|
|
27
|
+
* commit). Lets a field that keeps its entries as something other than text (attendees) read them without parsing
|
|
28
|
+
* `value` and guessing which part of it is still being typed. */
|
|
29
|
+
onEdit?: (chips: string[], pending: string) => void;
|
|
30
|
+
/** Text being typed already when the input mounts (the last recipient of `value`), so a field that is remounted
|
|
31
|
+
* (a popover growing into a form) keeps it in the input instead of turning it into a chip. */
|
|
32
|
+
initialPending?: string;
|
|
33
|
+
/** Chips this returns true for are not drawn (they stay in the value, and out of the suggestions): a field that
|
|
34
|
+
* lists some entries itself. Backspace in an empty input removes the last chip that is drawn. */
|
|
35
|
+
hideChip?: (chip: string) => boolean;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
/** The input's accessible name, for a field without a `<label htmlFor>`. */
|
|
38
|
+
ariaLabel?: string;
|
|
39
|
+
/** The chip list's accessible name; `${label} recipients` by default. */
|
|
40
|
+
listLabel?: string;
|
|
41
|
+
/** Classes for the field's outer element, after its own (a border, padding). */
|
|
42
|
+
className?: string;
|
|
43
|
+
/** The suggestions' `z-index`, for a field inside an overlay that stacks above the default's (`z-[60]`). */
|
|
44
|
+
dropdownZIndex?: number;
|
|
25
45
|
}
|
|
26
46
|
/**
|
|
27
47
|
* A To/Cc/Bcc field: committed recipients as removable chips, and a WAI-ARIA combobox for the one being typed that
|
|
@@ -33,4 +53,4 @@ export interface RecipientInputProps {
|
|
|
33
53
|
* least two characters, and a newer query aborts an older request. Arrow keys move through them, Enter or Tab picks
|
|
34
54
|
* one (as `Name <address>`), Escape closes them, and Backspace in an empty input removes the last chip.
|
|
35
55
|
*/
|
|
36
|
-
export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions, debounceMs, disabled, autoFocus, }: RecipientInputProps): React.JSX.Element;
|
|
56
|
+
export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions, debounceMs, disabled, autoFocus, onEdit, initialPending, hideChip, placeholder, ariaLabel, listLabel, className, dropdownZIndex, }: RecipientInputProps): React.JSX.Element;
|
|
@@ -41,8 +41,8 @@ function dropdownPosition(anchor) {
|
|
|
41
41
|
* least two characters, and a newer query aborts an older request. Arrow keys move through them, Enter or Tab picks
|
|
42
42
|
* one (as `Name <address>`), Escape closes them, and Backspace in an empty input removes the last chip.
|
|
43
43
|
*/
|
|
44
|
-
export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions = fetchRecipientSuggestions, debounceMs = RECIPIENT_SUGGESTION_DEBOUNCE_MS, disabled, autoFocus, }) {
|
|
45
|
-
const [pending, setPending] = useState(
|
|
44
|
+
export default function RecipientInput({ id, label, value, onChange, onBlur, onCommit, mailboxUid, fetchSuggestions = fetchRecipientSuggestions, debounceMs = RECIPIENT_SUGGESTION_DEBOUNCE_MS, disabled, autoFocus, onEdit, initialPending = "", hideChip, placeholder, ariaLabel, listLabel, className, dropdownZIndex, }) {
|
|
45
|
+
const [pending, setPending] = useState(initialPending);
|
|
46
46
|
const [suggestions, setSuggestions] = useState([]);
|
|
47
47
|
const [open, setOpen] = useState(false);
|
|
48
48
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
@@ -58,6 +58,8 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
|
|
|
58
58
|
const chips = pendingActive ? tokens.slice(0, -1) : tokens;
|
|
59
59
|
const inputValue = pendingActive ? pending : "";
|
|
60
60
|
const query = pendingActive ? pendingText : "";
|
|
61
|
+
const drawn = chips.map((chip, index) => ({ chip, index })).filter(({ chip }) => !hideChip?.(chip));
|
|
62
|
+
const lastDrawn = drawn.length > 0 ? drawn[drawn.length - 1].index : -1;
|
|
61
63
|
const listboxId = `${id}-suggestions`;
|
|
62
64
|
const optionId = (index) => `${id}-suggestion-${index}`;
|
|
63
65
|
useEffect(() => {
|
|
@@ -113,6 +115,7 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
|
|
|
113
115
|
});
|
|
114
116
|
function emit(nextChips, nextPending) {
|
|
115
117
|
onChange([...nextChips, nextPending.trim()].filter((part) => part.length > 0).join(", "));
|
|
118
|
+
onEdit?.(nextChips, nextPending.trim());
|
|
116
119
|
}
|
|
117
120
|
function handleInputChange(e) {
|
|
118
121
|
const { finished, rest } = splitTypedRecipients(e.target.value);
|
|
@@ -122,17 +125,22 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
|
|
|
122
125
|
emit([...chips, ...finished], nextPending);
|
|
123
126
|
}
|
|
124
127
|
function commitPending() {
|
|
125
|
-
const
|
|
128
|
+
const committed = [...chips, pendingText].filter((part) => part.length > 0);
|
|
126
129
|
setPending("");
|
|
127
130
|
setOpen(false);
|
|
128
|
-
|
|
131
|
+
if (pendingText) {
|
|
132
|
+
onEdit?.(committed, "");
|
|
133
|
+
}
|
|
134
|
+
return committed.join(", ");
|
|
129
135
|
}
|
|
130
136
|
function select(suggestion) {
|
|
131
|
-
const
|
|
137
|
+
const committed = [...chips, formatRecipient({ address: suggestion.address, displayName: suggestion.displayName || undefined })];
|
|
138
|
+
const next = committed.join(", ");
|
|
132
139
|
setPending("");
|
|
133
140
|
setOpen(false);
|
|
134
141
|
setSuggestions([]);
|
|
135
142
|
onChange(next);
|
|
143
|
+
onEdit?.(committed, "");
|
|
136
144
|
onCommit?.(next);
|
|
137
145
|
}
|
|
138
146
|
function removeChip(index) {
|
|
@@ -181,9 +189,9 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
|
|
|
181
189
|
}
|
|
182
190
|
return;
|
|
183
191
|
case "Backspace":
|
|
184
|
-
if (inputValue === "" &&
|
|
192
|
+
if (inputValue === "" && lastDrawn >= 0) {
|
|
185
193
|
e.preventDefault();
|
|
186
|
-
emit(chips.
|
|
194
|
+
emit(chips.filter((_, i) => i !== lastDrawn), "");
|
|
187
195
|
}
|
|
188
196
|
return;
|
|
189
197
|
}
|
|
@@ -192,12 +200,12 @@ export default function RecipientInput({ id, label, value, onChange, onBlur, onC
|
|
|
192
200
|
const next = commitPending();
|
|
193
201
|
onBlur?.(next);
|
|
194
202
|
}
|
|
195
|
-
return (_jsxs("div", { ref: anchorRef, className:
|
|
203
|
+
return (_jsxs("div", { ref: anchorRef, className: `flex-1 min-w-0 flex flex-wrap items-center gap-1${className ? ` ${className}` : ""}`, children: [drawn.length > 0 && (_jsx("ul", { role: "list", "aria-label": listLabel ?? `${label} recipients`, className: "contents", children: drawn.map(({ chip, index }) => {
|
|
196
204
|
const recipient = parseRecipient(chip);
|
|
197
205
|
const valid = isValidRecipientAddress(recipient.address);
|
|
198
206
|
const name = recipient.displayName || recipient.address;
|
|
199
207
|
return (_jsxs("li", { title: chip, className: `flex items-center gap-1 max-w-full min-w-0 rounded-pill border pl-2 pr-0.5 py-px text-xs ${valid ? "bg-surface-alt border-border text-text" : "bg-danger-bg border-danger text-danger"}`, children: [_jsx("span", { className: "truncate", children: name }), !valid && _jsx("span", { className: "sr-only", children: " (not a valid email address)" }), _jsx("button", { type: "button", tabIndex: -1, "aria-label": `Remove ${recipient.address}`, disabled: disabled, onMouseDown: (e) => e.preventDefault(), onClick: () => removeChip(index), className: "shrink-0 w-4 h-4 flex items-center justify-center rounded-pill text-text-muted hover:bg-border hover:text-text", children: _jsx(HiOutlineXMark, { size: 10 }) })] }, `${index}-${chip}`));
|
|
200
|
-
}) })), _jsx("input", { ref: inputRef, id: id, type: "text", role: "combobox", autoComplete: "off", "aria-autocomplete": "list", "aria-expanded": expanded, "aria-controls": listboxId, "aria-activedescendant": expanded ? optionId(active) : undefined, disabled: disabled, autoFocus: autoFocus, className: "flex-1 min-w-[6rem] text-sm bg-transparent outline-none py-0.5", value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onBlur: handleBlur }), _jsx("span", { className: "sr-only", "aria-live": "polite", children: expanded ? `${visible.length} ${visible.length === 1 ? "suggestion" : "suggestions"}` : "" }), expanded &&
|
|
208
|
+
}) })), _jsx("input", { ref: inputRef, id: id, type: "text", role: "combobox", autoComplete: "off", "aria-autocomplete": "list", "aria-label": ariaLabel, placeholder: placeholder, "aria-expanded": expanded, "aria-controls": listboxId, "aria-activedescendant": expanded ? optionId(active) : undefined, disabled: disabled, autoFocus: autoFocus, className: "flex-1 min-w-[6rem] text-sm bg-transparent outline-none py-0.5", value: inputValue, onChange: handleInputChange, onKeyDown: handleKeyDown, onBlur: handleBlur }), _jsx("span", { className: "sr-only", "aria-live": "polite", children: expanded ? `${visible.length} ${visible.length === 1 ? "suggestion" : "suggestions"}` : "" }), expanded &&
|
|
201
209
|
position &&
|
|
202
|
-
createPortal(_jsx("ul", { id: listboxId, role: "listbox", "aria-label": `${label} suggestions`, onMouseDown: (e) => e.preventDefault(), style: { position: "fixed", left: position.left, width: position.width, top: position.top, bottom: position.bottom, maxHeight: position.maxHeight }, className: "z-[60] overflow-y-auto bg-surface border border-border rounded-md shadow-modal py-1", children: visible.map((suggestion, index) => (_jsxs("li", { id: optionId(index), role: "option", "aria-selected": index === active, onMouseDown: (e) => e.preventDefault(), onMouseEnter: () => setActiveIndex(index), onClick: () => select(suggestion), className: `flex items-center gap-2 px-3 py-1.5 cursor-pointer ${index === active ? "bg-primary/10" : ""}`, children: [_jsxs("span", { className: "flex-1 min-w-0", children: [_jsx("span", { className: "block truncate text-sm text-text", children: suggestion.displayName || suggestion.address }), suggestion.displayName && _jsx("span", { className: "block truncate text-xs text-text-muted", children: suggestion.address })] }), _jsx("span", { className: "shrink-0 text-[11px] leading-4 px-1.5 rounded-pill bg-surface-alt text-text-muted", children: KIND_LABELS[suggestion.kind] ?? "Directory" })] }, `${index}-${suggestion.address}`))) }), document.body)] }));
|
|
210
|
+
createPortal(_jsx("ul", { id: listboxId, role: "listbox", "aria-label": `${label} suggestions`, onMouseDown: (e) => e.preventDefault(), style: { position: "fixed", left: position.left, width: position.width, top: position.top, bottom: position.bottom, maxHeight: position.maxHeight, zIndex: dropdownZIndex }, className: "z-[60] overflow-y-auto bg-surface border border-border rounded-md shadow-modal py-1", children: visible.map((suggestion, index) => (_jsxs("li", { id: optionId(index), role: "option", "aria-selected": index === active, onMouseDown: (e) => e.preventDefault(), onMouseEnter: () => setActiveIndex(index), onClick: () => select(suggestion), className: `flex items-center gap-2 px-3 py-1.5 cursor-pointer ${index === active ? "bg-primary/10" : ""}`, children: [_jsxs("span", { className: "flex-1 min-w-0", children: [_jsx("span", { className: "block truncate text-sm text-text", children: suggestion.displayName || suggestion.address }), suggestion.displayName && _jsx("span", { className: "block truncate text-xs text-text-muted", children: suggestion.address })] }), _jsx("span", { className: "shrink-0 text-[11px] leading-4 px-1.5 rounded-pill bg-surface-alt text-text-muted", children: KIND_LABELS[suggestion.kind] ?? "Directory" })] }, `${index}-${suggestion.address}`))) }), document.body)] }));
|
|
203
211
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FolderBadge } from "../../../mail/folderCounts.js";
|
|
3
|
+
/** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
|
|
4
|
+
export default function FolderBadgeChip({ badge }: {
|
|
5
|
+
badge: FolderBadge;
|
|
6
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { badgeLabel } from "../../../mail/folderCounts.js";
|
|
3
|
+
/** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
|
|
4
|
+
export default function FolderBadgeChip({ badge }) {
|
|
5
|
+
return (_jsxs("span", { className: [
|
|
6
|
+
"text-xs rounded-pill py-0.5 px-1.5",
|
|
7
|
+
badge.kind === "unread" ? "font-bold bg-primary/15 text-primary-dark" : "font-medium text-text-muted",
|
|
8
|
+
].join(" "), children: [_jsx("span", { "aria-hidden": "true", children: badge.value }), _jsxs("span", { className: "sr-only", children: [" ", badgeLabel(badge)] })] }));
|
|
9
|
+
}
|
|
@@ -3,7 +3,7 @@ import { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js"
|
|
|
3
3
|
import { AppShellProps } from "../../layout/AppShell.js";
|
|
4
4
|
import { LiveUpdates } from "../../../mail/useMailLiveUpdates.js";
|
|
5
5
|
import { MAILBOX_LIST_LIMIT } from "../../../mail/useMailConnection.js";
|
|
6
|
-
import { CountTracker } from "../../../mail/folderCounts.js";
|
|
6
|
+
import { CountTracker, FolderCount } from "../../../mail/folderCounts.js";
|
|
7
7
|
export type MailShellProps = Omit<AppShellProps, "active">;
|
|
8
8
|
/** One mailbox's own mail folders (already filtered to `MAIL_FOLDER_TYPES` and unsorted) - one entry per
|
|
9
9
|
* mailbox in `mailboxes`, fetched in parallel so every accessible mailbox's folder tree can render
|
|
@@ -59,6 +59,13 @@ export interface MailShellContextValue {
|
|
|
59
59
|
* context value, which is only ever read outside a real shell.
|
|
60
60
|
*/
|
|
61
61
|
trackMessageChange: (previous: Message, next: Message | null) => CountTracker;
|
|
62
|
+
/**
|
|
63
|
+
* Reads every mailbox's real folder counts back and shows them (after `delayMs`, 600 ms by default) - for a change too big to track message by
|
|
64
|
+
* message, such as emptying a folder. A no-op on the default context value, which is only ever read outside a real shell.
|
|
65
|
+
*/
|
|
66
|
+
refreshFolderCounts: (delayMs?: number) => void;
|
|
67
|
+
/** A folder's counts as the sidebar shows them: what the shell has worked out or been told since it loaded, else what the folder was listed with. */
|
|
68
|
+
folderCountOf: (folder: Folder) => FolderCount;
|
|
62
69
|
/**
|
|
63
70
|
* The phone layout's header row beside the folders button, for the page to put its search box in (`createPortal()`): the shell
|
|
64
71
|
* owns the row, the page owns the search state. `null` until the row has rendered, and on the default context value.
|
|
@@ -71,17 +78,19 @@ export { MAILBOX_LIST_LIMIT };
|
|
|
71
78
|
/**
|
|
72
79
|
* Mail's own contextual sidebar (every accessible mailbox's own folder tree, plus a merged "All Mailboxes"
|
|
73
80
|
* aggregate section) + content area, rendered inside the shared `AppShell` chrome (icon rail, header,
|
|
74
|
-
* impersonation banner — see that component).
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* `apps/admin/quarantine`'s `readMailboxUid()` — so the server-rendered and just-hydrated client markup
|
|
79
|
-
* match).
|
|
81
|
+
* impersonation banner — see that component). The selected mailbox/folder (or aggregate pseudo-folder) live
|
|
82
|
+
* in the URL's `?mailboxUid=`/`?folderUid=`/`?aggregate=` query params, read from the router's location
|
|
83
|
+
* (`@rapidrest/react`), which is the request's on the server and while hydrating and follows a shallow
|
|
84
|
+
* navigation (a folder link) without remounting the page.
|
|
80
85
|
*
|
|
81
86
|
* Every accessible mailbox's folder tree renders at once - there is no "switch mailbox" affordance
|
|
82
87
|
* anymore (the mailbox `<select>` this shell used to have is gone) - so a single-mailbox user sees zero
|
|
83
88
|
* behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
|
|
84
89
|
* aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
|
|
85
90
|
* of folders" requirement.
|
|
91
|
+
*
|
|
92
|
+
* An address that names nothing opens "All Mailboxes > Inbox" when there is more than one mailbox (that is when the section exists), and
|
|
93
|
+
* the one mailbox's own Inbox otherwise; a `?mailboxUid=`, `?folderUid=` or `?aggregate=` always wins. What needs a single mailbox while an all-mailboxes view is open (Compose, the new-message shortcut, key unlocking) uses the
|
|
94
|
+
* primary mailbox.
|
|
86
95
|
*/
|
|
87
96
|
export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }: PropsWithChildren<MailShellProps>): React.JSX.Element;
|