@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
|
@@ -3,13 +3,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
7
7
|
import { HiOutlineBars3, HiOutlinePencilSquare } from "react-icons/hi2";
|
|
8
8
|
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
9
9
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
10
10
|
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
11
11
|
import AppShell from "../../layout/AppShell.js";
|
|
12
|
-
import
|
|
12
|
+
import FloatingActionButton from "../../layout/FloatingActionButton.js";
|
|
13
|
+
import { useLocation } from "@rapidrest/react/client";
|
|
14
|
+
import { useNavigate } from "../../../navigation/index.js";
|
|
13
15
|
import KeyEnrollmentGate from "../../layout/KeyEnrollmentGate.js";
|
|
14
16
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
15
17
|
import { prefetchComposeWindow, useCompose } from "../compose/ComposeContext.js";
|
|
@@ -17,7 +19,11 @@ import LocalIndexLifecycle from "../../../search/LocalIndexLifecycle.js";
|
|
|
17
19
|
import { NO_LIVE_UPDATES } from "../../../mail/useMailLiveUpdates.js";
|
|
18
20
|
import { MAILBOX_LIST_LIMIT, MailConnectionContext, useMailConnection } from "../../../mail/useMailConnection.js";
|
|
19
21
|
import { folderRows } from "../../../mail/folderTree.js";
|
|
20
|
-
import {
|
|
22
|
+
import { primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
23
|
+
import { badgeFor, countOfFolder, inboxUnreadTotal, unreadBadgeTotal, } from "../../../mail/folderCounts.js";
|
|
24
|
+
import { ALL_MAILBOXES_SECTION, useSidebarSections } from "../../../mail/useCollapsedSections.js";
|
|
25
|
+
import FolderBadgeChip from "./FolderBadgeChip.js";
|
|
26
|
+
import SidebarSection from "./SidebarSection.js";
|
|
21
27
|
import { useUnreadTitle } from "../../../mail/useUnreadTitle.js";
|
|
22
28
|
import { pendingCountFor, usePendingSends } from "../../../mail/outbox/pendingSends.js";
|
|
23
29
|
import OutboxBadge from "../OutboxBadge.js";
|
|
@@ -42,6 +48,8 @@ const MailShellContext = createContext({
|
|
|
42
48
|
noteFolderUids: () => undefined,
|
|
43
49
|
live: NO_LIVE_UPDATES,
|
|
44
50
|
trackMessageChange: () => NO_TRACKER,
|
|
51
|
+
refreshFolderCounts: () => undefined,
|
|
52
|
+
folderCountOf: (folder) => ({ unread: folder.unreadCount, total: folder.totalCount }),
|
|
45
53
|
mobileSearchSlot: null,
|
|
46
54
|
});
|
|
47
55
|
/** Reads the mailbox/folder a page is currently showing, as resolved by the enclosing `MailShell`. */
|
|
@@ -66,13 +74,6 @@ function aggregateBadge(mailboxFolders, type, counts) {
|
|
|
66
74
|
}, { unread: 0, total: 0 });
|
|
67
75
|
return badgeFor(type, sum);
|
|
68
76
|
}
|
|
69
|
-
/** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
|
|
70
|
-
function FolderBadgeChip({ badge }) {
|
|
71
|
-
return (_jsxs("span", { className: [
|
|
72
|
-
"text-xs rounded-pill py-0.5 px-1.5",
|
|
73
|
-
badge.kind === "unread" ? "font-bold bg-primary/15 text-primary-dark" : "font-medium text-text-muted",
|
|
74
|
-
].join(" "), children: [_jsx("span", { "aria-hidden": "true", children: badge.value }), _jsxs("span", { className: "sr-only", children: [" ", badgeLabel(badge)] })] }));
|
|
75
|
-
}
|
|
76
77
|
export { MAILBOX_LIST_LIMIT };
|
|
77
78
|
/**
|
|
78
79
|
* A separate component (not inlined into `MailShell`'s own render) so `useCompose()` resolves against
|
|
@@ -98,7 +99,7 @@ function ComposeButton({ mailboxUid }) {
|
|
|
98
99
|
*/
|
|
99
100
|
function MobileComposeButton({ mailboxUid }) {
|
|
100
101
|
const { openCompose } = useCompose();
|
|
101
|
-
return (_jsx(
|
|
102
|
+
return (_jsx(FloatingActionButton, { label: "New message", icon: HiOutlinePencilSquare, onClick: () => openCompose({ mailboxUid }), onPointerEnter: prefetchComposeWindow, onFocus: prefetchComposeWindow }));
|
|
102
103
|
}
|
|
103
104
|
/**
|
|
104
105
|
* Mail's keyboard shortcuts that don't belong to a list or a message: "New message". A component of its own for the reason `ComposeButton`
|
|
@@ -113,18 +114,20 @@ function MailShortcuts({ mailboxUid }) {
|
|
|
113
114
|
/**
|
|
114
115
|
* Mail's own contextual sidebar (every accessible mailbox's own folder tree, plus a merged "All Mailboxes"
|
|
115
116
|
* aggregate section) + content area, rendered inside the shared `AppShell` chrome (icon rail, header,
|
|
116
|
-
* impersonation banner — see that component).
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* `apps/admin/quarantine`'s `readMailboxUid()` — so the server-rendered and just-hydrated client markup
|
|
121
|
-
* match).
|
|
117
|
+
* impersonation banner — see that component). The selected mailbox/folder (or aggregate pseudo-folder) live
|
|
118
|
+
* in the URL's `?mailboxUid=`/`?folderUid=`/`?aggregate=` query params, read from the router's location
|
|
119
|
+
* (`@rapidrest/react`), which is the request's on the server and while hydrating and follows a shallow
|
|
120
|
+
* navigation (a folder link) without remounting the page.
|
|
122
121
|
*
|
|
123
122
|
* Every accessible mailbox's folder tree renders at once - there is no "switch mailbox" affordance
|
|
124
123
|
* anymore (the mailbox `<select>` this shell used to have is gone) - so a single-mailbox user sees zero
|
|
125
124
|
* behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
|
|
126
125
|
* aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
|
|
127
126
|
* of folders" requirement.
|
|
127
|
+
*
|
|
128
|
+
* An address that names nothing opens "All Mailboxes > Inbox" when there is more than one mailbox (that is when the section exists), and
|
|
129
|
+
* 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
|
|
130
|
+
* primary mailbox.
|
|
128
131
|
*/
|
|
129
132
|
export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }) {
|
|
130
133
|
// The mailboxes, their folders and the live connection come from the persistent app frame when there is one (`AppChrome` owns them, so
|
|
@@ -136,51 +139,60 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
136
139
|
const { status, error, mailboxes, mailboxFolders, foldersLoading, onFolderCreated, noteFolderUids, live, folderCounts, outbox: outboxStatuses } = hosted ?? own;
|
|
137
140
|
// The messages this tab is still handing to the server: the Outbox pill counts them at once.
|
|
138
141
|
const pendingSends = usePendingSends();
|
|
139
|
-
const [requestedMailboxUid, setRequestedMailboxUid] = useState(null);
|
|
140
|
-
const [requestedFolderUid, setRequestedFolderUid] = useState(null);
|
|
141
|
-
const [requestedAggregateType, setRequestedAggregateType] = useState(null);
|
|
142
142
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
143
|
-
// The selection lives in the URL, and the router (`
|
|
144
|
-
// clicked - so it is read from the router's location, which updates,
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
// The selection lives in the URL, and the router (`@rapidrest/react`) changes the URL without a page load when a folder link is
|
|
144
|
+
// clicked - a shallow navigation, which keeps this page and its state - so it is read from the router's location, which updates,
|
|
145
|
+
// rather than once from `window.location`. The location is the request's on the server and while hydrating (its `#fragment` is not
|
|
146
|
+
// sent, and no one here reads it), so the server render and the hydrating render agree and the selection is known from the first render.
|
|
147
|
+
const { search } = useLocation();
|
|
148
|
+
const { requestedMailboxUid, requestedFolderUid, requestedAggregateType } = useMemo(() => {
|
|
148
149
|
const params = new URLSearchParams(search);
|
|
149
|
-
|
|
150
|
-
setRequestedFolderUid(params.get("folderUid"));
|
|
151
|
-
setRequestedAggregateType(params.get("aggregate"));
|
|
150
|
+
return { requestedMailboxUid: params.get("mailboxUid"), requestedFolderUid: params.get("folderUid"), requestedAggregateType: params.get("aggregate") };
|
|
152
151
|
}, [search]);
|
|
153
152
|
// Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
|
|
154
153
|
// with the page - is closed here.
|
|
155
154
|
useEffect(() => {
|
|
156
155
|
setDrawerOpen(false);
|
|
157
156
|
}, [search]);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
157
|
+
// What the address asks for.
|
|
158
|
+
const requestedAggregate = isAggregateFolderType(requestedAggregateType) ? requestedAggregateType : undefined;
|
|
159
|
+
const requestedMailbox = requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined;
|
|
160
|
+
// The mailbox the caller has not chosen: their own, never a shared one that merely sorts first. It is what compose, the new-message
|
|
161
|
+
// shortcut and everything else that needs a single mailbox use while an all-mailboxes view (which has none) is open.
|
|
162
|
+
const defaultMailboxUid = primaryMailboxUid(mailboxes, userUid);
|
|
163
|
+
// With several mailboxes the address that names nothing opens "All Mailboxes > Inbox", the merged Inbox; with one there is no such
|
|
164
|
+
// section, so it opens that mailbox's own Inbox. A mailbox, a folder or an all-mailboxes view in the address wins over either.
|
|
165
|
+
const aggregateByDefault = !requestedAggregate && !requestedMailbox && requestedFolderUid === null && mailboxes.length > 1;
|
|
166
|
+
const aggregateFolderType = requestedAggregate ?? (aggregateByDefault ? "inbox" : undefined);
|
|
167
|
+
const mailboxUid = aggregateFolderType ? undefined : (requestedMailbox ?? defaultMailboxUid);
|
|
165
168
|
const selectedMailboxFolders = mailboxFolders.find((mf) => mf.mailbox.uid === mailboxUid)?.folders ?? [];
|
|
166
169
|
const folderUid = aggregateFolderType
|
|
167
170
|
? undefined
|
|
168
171
|
: (requestedFolderUid && selectedMailboxFolders.some((f) => f.uid === requestedFolderUid) ? requestedFolderUid : undefined) ??
|
|
169
172
|
selectedMailboxFolders.find((f) => f.type === "inbox")?.uid;
|
|
170
173
|
// The mailbox `KeyEnrollmentGate`/`LocalIndexLifecycle`/`ComposeButton` treat as "the" mailbox when
|
|
171
|
-
// there's no single selected one to use (aggregate mode) - the caller's own
|
|
172
|
-
// have one, else whichever accessible mailbox happens to be first. See MailShell's own doc comment on
|
|
174
|
+
// there's no single selected one to use (aggregate mode) - the caller's primary mailbox (`primaryMailbox()`). See MailShell's own doc comment on
|
|
173
175
|
// this being an accepted limitation: an aggregate-view message from a *different*, not-yet-visited
|
|
174
176
|
// mailbox may still need that mailbox's own folder view opened directly to unlock/decrypt it.
|
|
175
|
-
const defaultMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
|
|
176
177
|
const activeMailboxUid = mailboxUid ?? defaultMailboxUid;
|
|
178
|
+
// Which sidebar sections are open: "All mailboxes" and the primary mailbox unless the reader chose otherwise. Any of them can be collapsed;
|
|
179
|
+
// a change of the open folder into another section opens that one (see `useSidebarSections()`); the first selection, once the address has been read
|
|
180
|
+
// and the mailboxes are here, is where a load lands, not a navigation. A lone mailbox has nothing to collapse, so its section is never a toggle.
|
|
181
|
+
const sections = useSidebarSections({
|
|
182
|
+
userUid,
|
|
183
|
+
primaryUid: defaultMailboxUid,
|
|
184
|
+
activeId: aggregateFolderType ? ALL_MAILBOXES_SECTION : mailboxUid,
|
|
185
|
+
});
|
|
186
|
+
const collapsible = mailboxes.length > 1;
|
|
177
187
|
const counts = folderCounts.counts;
|
|
178
188
|
const trackMessageChange = folderCounts.track;
|
|
189
|
+
const refreshFolderCounts = folderCounts.refresh;
|
|
190
|
+
const folderCountOf = useCallback((folder) => countOfFolder(folder, counts), [counts]);
|
|
179
191
|
// `(3) Acme: Mail` in the tab strip while there is unread mail in an Inbox - kept by the frame when there is one.
|
|
180
192
|
useUnreadTitle(inboxUnreadTotal(mailboxFolders, counts), { enabled: !hosted });
|
|
181
193
|
// A state rather than a ref: the page reads it during its render, so it has to re-render once the row it names is in the document.
|
|
182
194
|
const [mobileSearchSlot, setMobileSearchSlot] = useState(null);
|
|
183
|
-
const contextValue = useMemo(() => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, mobileSearchSlot }), [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, mobileSearchSlot]);
|
|
195
|
+
const contextValue = useMemo(() => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, refreshFolderCounts, folderCountOf, mobileSearchSlot }), [mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, refreshFolderCounts, folderCountOf, mobileSearchSlot]);
|
|
184
196
|
// A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else for a
|
|
185
197
|
// mailbox-less caller to do here yet, so the icon rail/header/folder tree don't render at all. Checks
|
|
186
198
|
// `mailboxes.length` directly (not `!mailboxUid`) since `mailboxUid` is legitimately undefined in
|
|
@@ -201,32 +213,33 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
201
213
|
// A function, not a plain JSX constant — it's rendered twice (desktop `<aside>` + mobile
|
|
202
214
|
// `Drawer`), possibly *simultaneously* mounted (the aside is only CSS-hidden below `md`, not
|
|
203
215
|
// unmounted).
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
216
|
+
// `idPrefix` keeps each section's folder list's id (which its heading's `aria-controls` names) unique between the two.
|
|
217
|
+
const sidebarContent = (idPrefix) => (_jsxs(_Fragment, { children: [_jsx("div", { className: "p-3", children: _jsx(ComposeButton, { mailboxUid: defaultMailboxUid }) }), foldersLoading ? (_jsx("div", { className: "flex-1 overflow-y-auto px-3 pb-3", children: _jsx(SkeletonList, { count: 6, className: "pt-1" }) })) : (_jsxs("nav", { className: "flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3", children: [mailboxes.length > 1 && (_jsx(SidebarSection, { domId: `${idPrefix}-mail-section-${ALL_MAILBOXES_SECTION}`, label: "All Mailboxes", collapsible: collapsible, expanded: sections.isExpanded(ALL_MAILBOXES_SECTION), onToggle: () => sections.toggle(ALL_MAILBOXES_SECTION), unread: unreadBadgeTotal(mailboxFolders.flatMap((mf) => mf.folders), counts), children: AGGREGATE_FOLDER_TYPES.map((type) => {
|
|
218
|
+
const badge = aggregateBadge(mailboxFolders, type, counts);
|
|
219
|
+
return (_jsxs("a", { href: `/?aggregate=${encodeURIComponent(type)}`, "data-router-shallow": "", "data-router-prefetch": "false", className: [
|
|
220
|
+
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
221
|
+
aggregateFolderType === type
|
|
222
|
+
? "bg-primary/10 text-primary-dark font-semibold"
|
|
223
|
+
: "text-text hover:bg-surface-alt",
|
|
224
|
+
].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[type] }), badge && _jsx(FolderBadgeChip, { badge: badge })] }, type));
|
|
225
|
+
}) })), mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (_jsx(SidebarSection, { domId: `${idPrefix}-mail-section-${mailbox.uid}`, label: _jsxs(_Fragment, { children: [mailbox.displayName, mailbox.ownerUserUid ? "" : " (shared)"] }), collapsible: collapsible, expanded: sections.isExpanded(mailbox.uid), onToggle: () => sections.toggle(mailbox.uid), unread: unreadBadgeTotal(folders, counts), notice: mailboxError && (_jsx("div", { className: "px-2.5 pb-1", children: _jsx(Alert, { children: mailboxError }) })), children: folderRows(folders, pendingCountFor(mailbox.uid, pendingSends)).map((row) => {
|
|
226
|
+
if (row.kind === "placeholder") {
|
|
227
|
+
// A folder the server has not made (or told us about) yet - the Outbox and Sent Items are made with the first message
|
|
228
|
+
// sent - shown the moment a message is on its way. Nothing to open; the real folder replaces it when it arrives.
|
|
229
|
+
const sending = pendingCountFor(mailbox.uid, pendingSends);
|
|
230
|
+
return (_jsxs("div", { "data-folder-placeholder": row.type, className: "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5 text-text-muted", children: [_jsx("span", { children: FOLDER_LABELS[row.type] }), row.type === "outbox" && _jsx(OutboxBadge, { total: sending, pendingHere: sending })] }, `placeholder-${row.type}`));
|
|
231
|
+
}
|
|
232
|
+
const folder = row.folder;
|
|
233
|
+
const badge = badgeFor(folder.type, countOfFolder(folder, counts));
|
|
234
|
+
return (_jsxs("a", { href: `/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`, "data-router-shallow": "", "data-router-prefetch": "false", className: [
|
|
235
|
+
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
236
|
+
folder.uid === folderUid
|
|
237
|
+
? "bg-primary/10 text-primary-dark font-semibold"
|
|
238
|
+
: "text-text hover:bg-surface-alt",
|
|
239
|
+
].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[folder.type] ?? folder.name }), badge &&
|
|
240
|
+
(folder.type === "outbox" ? (_jsx(OutboxBadge, { total: badge.value, status: outboxStatuses[folder.uid], pendingHere: pendingCountFor(mailbox.uid, pendingSends) })) : (_jsx(FolderBadgeChip, { badge: badge })))] }, folder.uid));
|
|
241
|
+
}) }, mailbox.uid)))] }))] }));
|
|
242
|
+
inner = (_jsxs(_Fragment, { children: [_jsx(MailShortcuts, { mailboxUid: defaultMailboxUid }), _jsx("aside", { className: "hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Folders", fullScreen: true, children: _jsx("div", { className: "flex flex-col", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 overflow-y-auto", children: [_jsxs("div", { className: "md:hidden flex items-center gap-2 p-3", children: [_jsx("button", { type: "button", className: "shrink-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open folders", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("div", { ref: setMobileSearchSlot, className: "flex-1 min-w-0" })] }), _jsx(MobileComposeButton, { mailboxUid: defaultMailboxUid }), _jsx(MailShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
230
243
|
}
|
|
231
244
|
// Wraps AppShell unconditionally, at a stable tree position regardless of whether mailboxUid has
|
|
232
245
|
// resolved yet - KeyEnrollmentGate itself passes `children` through untouched until a real
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
export interface SidebarSectionProps {
|
|
3
|
+
/** Unique per rendering of the sidebar (it is on screen twice on a phone: beside the list and in the drawer): names the folder list the heading controls. */
|
|
4
|
+
domId: string;
|
|
5
|
+
/** The heading's text. */
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
/** Whether the heading is a toggle at all. Not for a lone mailbox, whose section has nothing to be hidden for. */
|
|
8
|
+
collapsible: boolean;
|
|
9
|
+
expanded: boolean;
|
|
10
|
+
onToggle: () => void;
|
|
11
|
+
/** The unread mail in the section's folders that carry an unread count (see `unreadBadgeTotal()`), shown on the heading while it is collapsed (and only when there is some) so new mail in a hidden section is still noticed. */
|
|
12
|
+
unread?: number;
|
|
13
|
+
/** Shown between the heading and the folders, collapsed or not: a failure to load the section's folders is not something to hide. */
|
|
14
|
+
notice?: ReactNode;
|
|
15
|
+
/** The section's folder rows. */
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* One section of Mail's sidebar ("All mailboxes", or one mailbox): a heading over a list of folders. When `collapsible` the heading is a
|
|
20
|
+
* disclosure button (`aria-expanded`, `aria-controls` the list, a chevron that turns) and the list is `hidden` while it is collapsed -
|
|
21
|
+
* kept in the document, so the button's `aria-controls` always names something and nothing is fetched or rebuilt on opening it.
|
|
22
|
+
*/
|
|
23
|
+
export default function SidebarSection({ domId, label, collapsible, expanded, onToggle, unread, notice, children }: SidebarSectionProps): React.JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HiChevronDown } from "react-icons/hi2";
|
|
3
|
+
import FolderBadgeChip from "./FolderBadgeChip.js";
|
|
4
|
+
/**
|
|
5
|
+
* One section of Mail's sidebar ("All mailboxes", or one mailbox): a heading over a list of folders. When `collapsible` the heading is a
|
|
6
|
+
* disclosure button (`aria-expanded`, `aria-controls` the list, a chevron that turns) and the list is `hidden` while it is collapsed -
|
|
7
|
+
* kept in the document, so the button's `aria-controls` always names something and nothing is fetched or rebuilt on opening it.
|
|
8
|
+
*/
|
|
9
|
+
export default function SidebarSection({ domId, label, collapsible, expanded, onToggle, unread = 0, notice, children }) {
|
|
10
|
+
const shown = !collapsible || expanded;
|
|
11
|
+
return (_jsxs("div", { children: [collapsible ? (_jsx("h2", { className: "mb-1", children: _jsxs("button", { type: "button", "aria-expanded": expanded, "aria-controls": domId, onClick: onToggle, className: [
|
|
12
|
+
"flex w-full items-center gap-1 rounded-sm px-2.5 py-0.5 text-left text-xs font-bold uppercase tracking-wide text-text-muted",
|
|
13
|
+
"focus-visible:outline-2 focus-visible:outline-primary",
|
|
14
|
+
"hover:bg-surface-alt hover:text-text",
|
|
15
|
+
].join(" "), children: [_jsx(HiChevronDown, { size: 14, "aria-hidden": "true", className: ["shrink-0 transition-transform", expanded ? "" : "-rotate-90"].join(" ") }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: label }), !expanded && unread > 0 && _jsx(FolderBadgeChip, { badge: { kind: "unread", value: unread } })] }) })) : (_jsx("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate", children: label })), notice, _jsx("div", { id: domId, hidden: !shown, className: shown ? "flex flex-col gap-0.5" : "hidden", children: children })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { MenuSectionSpec } from "../MenuButton.js";
|
|
3
|
+
/** What each row of the menu does - the reading pane's own handlers. */
|
|
4
|
+
export interface MessageMenuActions {
|
|
5
|
+
replyAll: () => void;
|
|
6
|
+
forward: () => void;
|
|
7
|
+
deleteMessage: () => void;
|
|
8
|
+
toggleRead: () => void;
|
|
9
|
+
toggleFlag: () => void;
|
|
10
|
+
reportJunk: () => void;
|
|
11
|
+
reportPhishing: () => void;
|
|
12
|
+
notJunk: () => void;
|
|
13
|
+
notJunkAndTrust: () => void;
|
|
14
|
+
blockSender: () => void;
|
|
15
|
+
neverBlockSender: () => void;
|
|
16
|
+
print: () => void;
|
|
17
|
+
viewSource: () => void;
|
|
18
|
+
viewDetails: () => void;
|
|
19
|
+
saveAsEml: () => void;
|
|
20
|
+
createRule: () => void;
|
|
21
|
+
}
|
|
22
|
+
export interface MessageMoreMenuProps {
|
|
23
|
+
actions: MessageMenuActions;
|
|
24
|
+
/** The sender's address the Block rows name. */
|
|
25
|
+
senderAddress: string;
|
|
26
|
+
read: boolean;
|
|
27
|
+
flagged: boolean;
|
|
28
|
+
/** The reader may change this mailbox's messages and filters (it is not a view-only share). */
|
|
29
|
+
writable: boolean;
|
|
30
|
+
/** Something is already being done to the message: its rows wait. */
|
|
31
|
+
busy: boolean;
|
|
32
|
+
inJunk: boolean;
|
|
33
|
+
/** The message is in Deleted Items: the Delete row reads "Delete permanently". */
|
|
34
|
+
inDeletedItems: boolean;
|
|
35
|
+
/** Mail the reader sent: it is not junk, and its sender is the reader. */
|
|
36
|
+
sent: boolean;
|
|
37
|
+
/** The sender is one of the reader's own addresses, which is not blocked. */
|
|
38
|
+
ownSender: boolean;
|
|
39
|
+
/** The reader may add a sender to this mailbox's Safe Senders: they have full access, or the server has not said which access they have (it then refuses, with its reason). */
|
|
40
|
+
canTrustSender: boolean;
|
|
41
|
+
/** Why the message cannot be printed right now (an encrypted one that is locked or still opening), or `undefined`. */
|
|
42
|
+
printReason: string | undefined;
|
|
43
|
+
/** The compose window is opening for a reply or forward: Reply all and Forward wait for it. */
|
|
44
|
+
composing: boolean;
|
|
45
|
+
/** The classes of the round "..." button - the reading pane's own icon-button look. */
|
|
46
|
+
triggerClassName: string;
|
|
47
|
+
}
|
|
48
|
+
/** The sections of the menu - Outlook's "More actions", cut to what this product really does. */
|
|
49
|
+
export declare function buildMessageMenu(props: MessageMoreMenuProps): MenuSectionSpec[];
|
|
50
|
+
/**
|
|
51
|
+
* The "..." at the end of a message card's icon row: Outlook's More actions menu, holding the actions there is no room for beside the
|
|
52
|
+
* message and the ones that are less often wanted. An ARIA menu (`MenuButton`): arrows, Home and End move, Right or Enter opens a submenu, Left or
|
|
53
|
+
* Escape leaves it (Escape at the top closes the menu and the focus goes back to this button), and a click outside closes it. A row that cannot be used right now
|
|
54
|
+
* is disabled and says why under its label.
|
|
55
|
+
*/
|
|
56
|
+
export default function MessageMoreMenu(props: MessageMoreMenuProps): React.JSX.Element;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { HiOutlineArrowDownTray, HiOutlineArrowUturnLeft, HiOutlineArrowUturnRight, HiOutlineCodeBracket, HiOutlineCog6Tooth, HiOutlineDocumentText, HiOutlineEllipsisHorizontal, HiOutlineEnvelope, HiOutlineEnvelopeOpen, HiOutlineEye, HiOutlineFlag, HiOutlineFunnel, HiOutlineHandRaised, HiOutlineInboxArrowDown, HiOutlineInformationCircle, HiOutlineNoSymbol, HiOutlinePrinter, HiOutlineShieldCheck, HiOutlineShieldExclamation, HiOutlineTrash, } from "react-icons/hi2";
|
|
3
|
+
import MenuButton from "../MenuButton.js";
|
|
4
|
+
const VIEW_ONLY = "View-only mailbox";
|
|
5
|
+
/** A row, disabled with `reason` (shown under the label, where a keyboard or phone user sees it as well) when there is one. */
|
|
6
|
+
function row(key, label, icon, onSelect, reason, extra = {}) {
|
|
7
|
+
return { key, label, icon, onSelect, disabled: reason !== undefined, description: reason, ...extra };
|
|
8
|
+
}
|
|
9
|
+
/** The sections of the menu - Outlook's "More actions", cut to what this product really does. */
|
|
10
|
+
export function buildMessageMenu(props) {
|
|
11
|
+
const { actions, read, flagged, writable, busy, inJunk, inDeletedItems, sent, ownSender, printReason, composing } = props;
|
|
12
|
+
// The reason a row that changes the message is unavailable, if it is: the first that applies wins.
|
|
13
|
+
const changeReason = !writable ? VIEW_ONLY : undefined;
|
|
14
|
+
const junkReason = changeReason ?? (sent ? "Not available for mail you sent" : inJunk ? "Already in Junk Email" : undefined);
|
|
15
|
+
const blockReason = changeReason ?? (sent ? "Not available for mail you sent" : ownSender ? "This is your own address" : undefined);
|
|
16
|
+
const waiting = (reason) => reason ?? (busy ? "Working on this message" : undefined);
|
|
17
|
+
const trustLabel = `Not junk, and always trust ${props.senderAddress}`;
|
|
18
|
+
const blockLabel = `Block ${props.senderAddress}`;
|
|
19
|
+
const neverBlockLabel = `Never block ${props.senderAddress}`;
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
key: "message",
|
|
23
|
+
items: [
|
|
24
|
+
{
|
|
25
|
+
key: "reply-actions",
|
|
26
|
+
label: "Other reply actions",
|
|
27
|
+
icon: _jsx(HiOutlineArrowUturnLeft, { size: 16 }),
|
|
28
|
+
submenu: [
|
|
29
|
+
{
|
|
30
|
+
key: "reply-actions-items",
|
|
31
|
+
items: [
|
|
32
|
+
row("reply-all", "Reply all", _jsx(HiOutlineArrowUturnLeft, { size: 16 }), actions.replyAll, composing ? "Opening the compose window" : undefined),
|
|
33
|
+
row("forward", "Forward", _jsx(HiOutlineArrowUturnRight, { size: 16 }), actions.forward, composing ? "Opening the compose window" : undefined),
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
row("delete",
|
|
39
|
+
// In Deleted Items there is nowhere further to move it to: Delete is the permanent one, and asks first.
|
|
40
|
+
inDeletedItems ? "Delete permanently" : "Delete", _jsx(HiOutlineTrash, { size: 16 }), actions.deleteMessage, waiting(changeReason)),
|
|
41
|
+
row("toggle-read", read ? "Mark as unread" : "Mark as read", read ? _jsx(HiOutlineEnvelope, { size: 16 }) : _jsx(HiOutlineEnvelopeOpen, { size: 16 }), actions.toggleRead, waiting(changeReason)),
|
|
42
|
+
row("toggle-flag", flagged ? "Unflag" : "Flag", _jsx(HiOutlineFlag, { size: 16 }), actions.toggleFlag, waiting(changeReason)),
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: "sender",
|
|
47
|
+
items: [
|
|
48
|
+
{
|
|
49
|
+
key: "report",
|
|
50
|
+
label: "Report",
|
|
51
|
+
icon: _jsx(HiOutlineNoSymbol, { size: 16 }),
|
|
52
|
+
submenu: [
|
|
53
|
+
{
|
|
54
|
+
key: "report-items",
|
|
55
|
+
items: [
|
|
56
|
+
row("report-junk", "Report junk", _jsx(HiOutlineNoSymbol, { size: 16 }), actions.reportJunk, waiting(junkReason)),
|
|
57
|
+
row("report-phishing", "Report phishing", _jsx(HiOutlineShieldExclamation, { size: 16 }), actions.reportPhishing, waiting(junkReason)),
|
|
58
|
+
// In Junk Email the way back: to the Inbox, telling the spam filter it was wrong - and, when the reader may, trusting the sender.
|
|
59
|
+
...(inJunk
|
|
60
|
+
? [
|
|
61
|
+
row("not-junk", "Not junk", _jsx(HiOutlineInboxArrowDown, { size: 16 }), actions.notJunk, waiting(changeReason)),
|
|
62
|
+
...(props.canTrustSender && !ownSender
|
|
63
|
+
? [
|
|
64
|
+
row("not-junk-trust", trustLabel, _jsx(HiOutlineShieldCheck, { size: 16 }), actions.notJunkAndTrust, waiting(changeReason), {
|
|
65
|
+
title: trustLabel,
|
|
66
|
+
}),
|
|
67
|
+
]
|
|
68
|
+
: []),
|
|
69
|
+
]
|
|
70
|
+
: []),
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
key: "block",
|
|
77
|
+
label: "Block",
|
|
78
|
+
icon: _jsx(HiOutlineHandRaised, { size: 16 }),
|
|
79
|
+
submenu: [
|
|
80
|
+
{
|
|
81
|
+
key: "block-items",
|
|
82
|
+
items: [
|
|
83
|
+
row("block-sender", blockLabel, _jsx(HiOutlineHandRaised, { size: 16 }), actions.blockSender, waiting(blockReason), { title: blockLabel }),
|
|
84
|
+
row("never-block-sender", neverBlockLabel, _jsx(HiOutlineShieldCheck, { size: 16 }), actions.neverBlockSender, waiting(blockReason), { title: neverBlockLabel }),
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: "output",
|
|
93
|
+
items: [
|
|
94
|
+
row("print", "Print", _jsx(HiOutlinePrinter, { size: 16 }), actions.print, printReason),
|
|
95
|
+
{
|
|
96
|
+
key: "view",
|
|
97
|
+
label: "View",
|
|
98
|
+
icon: _jsx(HiOutlineEye, { size: 16 }),
|
|
99
|
+
submenu: [
|
|
100
|
+
{
|
|
101
|
+
key: "view-items",
|
|
102
|
+
items: [
|
|
103
|
+
row("view-source", "View message source", _jsx(HiOutlineCodeBracket, { size: 16 }), actions.viewSource),
|
|
104
|
+
row("view-details", "Message details", _jsx(HiOutlineInformationCircle, { size: 16 }), actions.viewDetails),
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: "save-as",
|
|
111
|
+
label: "Save as",
|
|
112
|
+
icon: _jsx(HiOutlineArrowDownTray, { size: 16 }),
|
|
113
|
+
submenu: [
|
|
114
|
+
{
|
|
115
|
+
key: "save-as-items",
|
|
116
|
+
items: [
|
|
117
|
+
row("save-eml", "Save as .eml", _jsx(HiOutlineEnvelope, { size: 16 }), actions.saveAsEml),
|
|
118
|
+
row("save-pdf", "Save as PDF", _jsx(HiOutlineDocumentText, { size: 16 }), actions.print, printReason, {
|
|
119
|
+
description: printReason ?? "Opens the print dialog: choose Save as PDF",
|
|
120
|
+
}),
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
key: "advanced",
|
|
129
|
+
items: [
|
|
130
|
+
{
|
|
131
|
+
key: "advanced-actions",
|
|
132
|
+
label: "Advanced actions",
|
|
133
|
+
icon: _jsx(HiOutlineCog6Tooth, { size: 16 }),
|
|
134
|
+
submenu: [
|
|
135
|
+
{
|
|
136
|
+
key: "advanced-items",
|
|
137
|
+
items: [row("create-rule", "Create rule", _jsx(HiOutlineFunnel, { size: 16 }), actions.createRule, changeReason)],
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The "..." at the end of a message card's icon row: Outlook's More actions menu, holding the actions there is no room for beside the
|
|
147
|
+
* message and the ones that are less often wanted. An ARIA menu (`MenuButton`): arrows, Home and End move, Right or Enter opens a submenu, Left or
|
|
148
|
+
* Escape leaves it (Escape at the top closes the menu and the focus goes back to this button), and a click outside closes it. A row that cannot be used right now
|
|
149
|
+
* is disabled and says why under its label.
|
|
150
|
+
*/
|
|
151
|
+
export default function MessageMoreMenu(props) {
|
|
152
|
+
return (_jsx(MenuButton, { iconOnly: true, label: "More actions", "aria-label": "More actions", title: "More actions", icon: _jsx(HiOutlineEllipsisHorizontal, { size: 16, "aria-hidden": "true" }), className: props.triggerClassName, sections: buildMessageMenu(props), width: 288 }));
|
|
153
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/** What the dialog is showing a message's source for: the whole of it, or only its header lines ("Message details"). */
|
|
3
|
+
export type SourceMode = "source" | "headers";
|
|
4
|
+
/** What has been fetched of the raw message: on its way, here, or refused. */
|
|
5
|
+
export type SourceState = {
|
|
6
|
+
status: "loading";
|
|
7
|
+
} | {
|
|
8
|
+
status: "ready";
|
|
9
|
+
raw: string;
|
|
10
|
+
} | {
|
|
11
|
+
status: "error";
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
export interface MessageSourceDialogProps {
|
|
15
|
+
open: boolean;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
mode: SourceMode;
|
|
18
|
+
source: SourceState;
|
|
19
|
+
/** The message is end-to-end encrypted: what is shown is the ciphertext the server holds, and the dialog says so. */
|
|
20
|
+
encrypted: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A message's raw source (or just its headers) in a full-window dialog, in a monospace block that can be scrolled and selected, with a Copy
|
|
24
|
+
* button. It is the RFC 5322 text exactly as the server stores it - for an encrypted message that is the encrypted message, never a
|
|
25
|
+
* decrypted one, and the dialog says so. A very long source is cut for display (`MAX_SOURCE_DISPLAY_CHARS`); Copy has all of it.
|
|
26
|
+
*/
|
|
27
|
+
export default function MessageSourceDialog({ open, onClose, mode, source, encrypted }: MessageSourceDialogProps): React.JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
8
|
+
import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
|
|
9
|
+
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
10
|
+
import { MAX_SOURCE_DISPLAY_CHARS, headerText, sourceText } from "./messageExport.js";
|
|
11
|
+
const TITLES = { source: "Message source", headers: "Message details" };
|
|
12
|
+
/**
|
|
13
|
+
* A message's raw source (or just its headers) in a full-window dialog, in a monospace block that can be scrolled and selected, with a Copy
|
|
14
|
+
* button. It is the RFC 5322 text exactly as the server stores it - for an encrypted message that is the encrypted message, never a
|
|
15
|
+
* decrypted one, and the dialog says so. A very long source is cut for display (`MAX_SOURCE_DISPLAY_CHARS`); Copy has all of it.
|
|
16
|
+
*/
|
|
17
|
+
export default function MessageSourceDialog({ open, onClose, mode, source, encrypted }) {
|
|
18
|
+
const text = useMemo(() => {
|
|
19
|
+
if (source.status !== "ready") {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
return mode === "headers" ? headerText(source.raw) : sourceText(source.raw);
|
|
23
|
+
}, [source, mode]);
|
|
24
|
+
const cut = text.length > MAX_SOURCE_DISPLAY_CHARS;
|
|
25
|
+
const title = TITLES[mode];
|
|
26
|
+
return (_jsx(Drawer, { open: open, onClose: onClose, title: title, fullScreen: true, children: _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [encrypted && (_jsx("p", { role: "note", className: "py-2 px-3 rounded-sm bg-surface-alt text-text", children: "This message is encrypted. This is the encrypted message exactly as the server stores it; it is not decrypted here." })), source.status === "loading" && (_jsx("p", { role: "status", className: "text-text-muted", children: "Loading the message\u2026" })), source.status === "error" && _jsx(Alert, { children: source.message }), source.status === "ready" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx(CopyButton, { value: text, label: `Copy the ${mode === "headers" ? "message headers" : "message source"}` }), cut && (_jsxs("span", { className: "text-xs text-text-muted", children: ["Showing the first ", MAX_SOURCE_DISPLAY_CHARS.toLocaleString(), " of ", text.length.toLocaleString(), " characters. Copy and Save as .eml keep all of it."] }))] }), _jsx("pre", { tabIndex: 0, "aria-label": title, className: "m-0 max-h-[70vh] overflow-auto rounded-sm border border-border bg-surface-alt p-3 font-mono text-xs whitespace-pre-wrap break-all", children: cut ? text.slice(0, MAX_SOURCE_DISPLAY_CHARS) : text })] }))] }) }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { OutgoingReply } from "../../../mail/outbox/outgoingReplies.js";
|
|
3
|
+
/**
|
|
4
|
+
* A reply or forward that has just been sent, drawn where its message will be once the server has filed it: the sender, who it went to, its body as
|
|
5
|
+
* it was composed, and one line saying where it is - "Sending...", "Sent", or, when it did not go, "Not sent: <why>" with the same ways out the
|
|
6
|
+
* pop-up offers (Retry, Open draft ...). It reads like the message's own card, so the real message replaces it without the thread rearranging.
|
|
7
|
+
*
|
|
8
|
+
* The body goes through the reading pane's own sanitizing frame (`MessageBody`), exactly as a received message's does.
|
|
9
|
+
*/
|
|
10
|
+
export default function PendingMessageCard({ reply, headerRef, }: {
|
|
11
|
+
reply: OutgoingReply;
|
|
12
|
+
/** Registered so the pane can put the focus here when the message first appears. Focusable, but not in the tab order. */
|
|
13
|
+
headerRef: (node: HTMLHeadingElement | null) => void;
|
|
14
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
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 MailAddress, { RecipientLine } from "../MailAddress.js";
|
|
4
|
+
import MessageBody from "./MessageBody.js";
|
|
5
|
+
import { BODY_FONT_STYLE, CardShell, SenderAvatar } from "./MessageCard.js";
|
|
6
|
+
/**
|
|
7
|
+
* A reply or forward that has just been sent, drawn where its message will be once the server has filed it: the sender, who it went to, its body as
|
|
8
|
+
* it was composed, and one line saying where it is - "Sending...", "Sent", or, when it did not go, "Not sent: <why>" with the same ways out the
|
|
9
|
+
* pop-up offers (Retry, Open draft ...). It reads like the message's own card, so the real message replaces it without the thread rearranging.
|
|
10
|
+
*
|
|
11
|
+
* The body goes through the reading pane's own sanitizing frame (`MessageBody`), exactly as a received message's does.
|
|
12
|
+
*/
|
|
13
|
+
export default function PendingMessageCard({ reply, headerRef, }) {
|
|
14
|
+
const failed = reply.state === "failed";
|
|
15
|
+
return (_jsxs(CardShell, { children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [_jsx(SenderAvatar, { from: reply.sender }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-x-2 text-sm", children: [_jsx("h2", { ref: headerRef, tabIndex: -1, style: BODY_FONT_STYLE, className: "flex-1 min-w-[13rem] font-semibold outline-none focus-visible:outline-2 focus-visible:outline-primary", children: _jsx(MailAddress, { recipient: reply.sender }) }), _jsxs("div", { role: failed ? "alert" : "status", "data-testid": "pending-message-status", "data-state": reply.state, className: ["flex items-center gap-1.5 text-xs shrink-0", failed ? "font-semibold text-danger" : "text-text-muted"].join(" "), children: [reply.state === "sending" && _jsx("span", { "aria-hidden": "true", className: "rr-sending-dot h-1.5 w-1.5 shrink-0 rounded-full bg-primary" }), _jsx("span", { children: failed ? `Not sent: ${reply.failure}` : reply.state === "sending" ? "Sending…" : "Sent" })] }, failed ? "failed" : "progress")] }), _jsx(RecipientLine, { label: "To", recipients: reply.to }), _jsx(RecipientLine, { label: "Cc", recipients: reply.cc }), _jsx(RecipientLine, { label: "Bcc", recipients: reply.bcc })] })] }), failed && (_jsx("div", { className: "flex flex-wrap gap-2 px-4 pb-3", children: reply.actions.map((action) => (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: action.onClick, children: action.label }, action.label))) })), _jsx("div", { className: "px-4 pb-4", children: _jsx(MessageBody, { messageUid: reply.uid, messageVersion: 0, title: `Message: ${reply.subject}`, content: { kind: "html", html: reply.html } }) })] }));
|
|
16
|
+
}
|