@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
|
@@ -11,8 +11,10 @@ import { listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
|
11
11
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
12
|
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
13
13
|
import AppShell from "../../layout/AppShell.js";
|
|
14
|
-
import {
|
|
14
|
+
import { useLocation } from "@rapidrest/react/client";
|
|
15
|
+
import { useNavigate } from "../../../navigation/index.js";
|
|
15
16
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
17
|
+
import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
16
18
|
import { mergePluginNavItems } from "../../../plugins/pluginNav.js";
|
|
17
19
|
/** Every settings section with its own sidebar entry. */
|
|
18
20
|
export const SETTINGS_SECTIONS = [
|
|
@@ -20,6 +22,7 @@ export const SETTINGS_SECTIONS = [
|
|
|
20
22
|
{ id: "appearance", href: "/settings/appearance", label: "Appearance" },
|
|
21
23
|
{ id: "auto-reply", href: "/settings/auto-reply", label: "Automatic Replies" },
|
|
22
24
|
{ id: "filters", href: "/settings/filters", label: "Mail Filters" },
|
|
25
|
+
{ id: "blocked-senders", href: "/settings/blocked-senders", label: "Blocked & Safe Senders" },
|
|
23
26
|
{ id: "signatures", href: "/settings/signatures", label: "Signatures" },
|
|
24
27
|
{ id: "labels", href: "/settings/labels", label: "Labels" },
|
|
25
28
|
{ id: "read-receipts", href: "/settings/read-receipts", label: "Read Receipts" },
|
|
@@ -57,7 +60,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
57
60
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
58
61
|
// Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
|
|
59
62
|
// effect, not during render, so the server render and the hydrating render agree.
|
|
60
|
-
const search =
|
|
63
|
+
const { search } = useLocation();
|
|
61
64
|
const navigate = useNavigate();
|
|
62
65
|
useEffect(() => {
|
|
63
66
|
setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
|
|
@@ -73,7 +76,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
73
76
|
}
|
|
74
77
|
listMailboxes({ limit: 100 })
|
|
75
78
|
.then((result) => {
|
|
76
|
-
setMailboxes(result);
|
|
79
|
+
setMailboxes(orderMailboxes(result, userUid));
|
|
77
80
|
setStatus("ready");
|
|
78
81
|
})
|
|
79
82
|
.catch((err) => {
|
|
@@ -82,7 +85,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
82
85
|
});
|
|
83
86
|
}, [userUid]);
|
|
84
87
|
const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
|
|
85
|
-
mailboxes
|
|
88
|
+
primaryMailboxUid(mailboxes, userUid);
|
|
86
89
|
const contextValue = useMemo(() => ({ mailboxUid, mailboxes }), [mailboxUid, mailboxes]);
|
|
87
90
|
// A full-screen takeover, not nested inside the rest of the app's chrome — same as ContactsShell/
|
|
88
91
|
// TasksShell, there's nothing else for a mailbox-less caller to configure here yet.
|
|
@@ -110,8 +113,8 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
110
113
|
}, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsx("nav", { "aria-label": "Settings sections", className: "flex flex-col gap-0.5", children: sections.map((section) => (_jsx("a", {
|
|
111
114
|
// `inner` (this whole branch) is only ever computed once `status === "ready"`
|
|
112
115
|
// and `mailboxUid` has already resolved truthy — a falsy `mailboxUid` returns
|
|
113
|
-
// `<MailboxProvisioning />` above instead (
|
|
114
|
-
//
|
|
116
|
+
// `<MailboxProvisioning />` above instead (which is what the `!` below records,
|
|
117
|
+
// `primaryMailboxUid()` being honest that it may find none) — so no fallback is
|
|
115
118
|
// needed here, matching this plan's established "dead guard the UI
|
|
116
119
|
// structurally can't trigger" removal precedent (e.g.
|
|
117
120
|
// `DomainDetailContent.handleVerify`).
|
|
@@ -119,7 +122,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
119
122
|
"block px-2.5 py-1.5 rounded-sm text-sm",
|
|
120
123
|
section.id === active ? "bg-primary/10 text-primary-dark font-medium" : "text-text hover:bg-surface-alt",
|
|
121
124
|
].join(" "), children: section.label }, section.id))) })] }));
|
|
122
|
-
inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Settings", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [_jsx("button", { type: "button", className: "md:hidden m-3 self-start w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open settings menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(SettingsShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
125
|
+
inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Settings", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [_jsx("button", { type: "button", className: "md:hidden m-3 self-start w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open settings menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(SettingsShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
123
126
|
}
|
|
124
127
|
return (_jsx(AppShell, { active: "settings", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
|
|
125
128
|
}
|
|
@@ -79,5 +79,5 @@ export default function TasksSidebar({ mailboxUid, tasks, userUid, active, onSel
|
|
|
79
79
|
// `Drawer` (see `ContactsSidebar` for the identical pattern and the duplicate-id reasoning behind
|
|
80
80
|
// the `idPrefix`).
|
|
81
81
|
const navContent = (idPrefix) => (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(NavItem, { label: "My Day", count: myDayCount, active: active.type === "myDay", onClick: () => handleSelect({ type: "myDay" }) }), _jsx(NavItem, { label: "Important", count: importantCount, active: active.type === "important", onClick: () => handleSelect({ type: "important" }) }), _jsx(NavItem, { label: "Planned", count: plannedCount, active: active.type === "planned", onClick: () => handleSelect({ type: "planned" }) }), _jsx(NavItem, { label: "Assigned to me", count: assignedToMeCount, active: active.type === "assignedToMe", onClick: () => handleSelect({ type: "assignedToMe" }) }), _jsx(NavItem, { label: "Flagged email", active: active.type === "flagged", onClick: () => handleSelect({ type: "flagged" }) })] }), _jsx(NavItem, { label: "Tasks", count: tasks.length, active: active.type === "all", onClick: () => handleSelect({ type: "all" }) }), _jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between px-2.5 mb-1", children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted", children: "Lists" }), _jsx("button", { type: "button", onClick: () => setAddingList(true), "aria-label": "New list", className: "text-xs font-bold text-primary-dark hover:underline", children: "+" })] }), listsError && _jsx("p", { className: "px-2.5 text-xs text-danger", children: listsError }), _jsx("div", { className: "flex flex-col gap-0.5", children: lists.map((list) => (_jsx(NavItem, { label: list.name, count: listCounts.get(list.uid) ?? 0, active: active.type === "list" && active.uid === list.uid, onClick: () => handleSelect({ type: "list", uid: list.uid, name: list.name }) }, list.uid))) }), addingList && (_jsxs("form", { onSubmit: handleAddList, className: "flex items-center gap-1 px-2.5 mt-1", children: [_jsx("input", { type: "text", autoFocus: true, "aria-label": "New list name", id: `${idPrefix}-new-list-name`, value: newListName, onChange: (e) => setNewListName(e.target.value), className: "flex-1 text-xs py-1 px-1.5 border border-border rounded-sm bg-surface" }), _jsx("button", { type: "submit", className: "text-xs font-semibold text-primary-dark", children: "Add" })] }))] })] }));
|
|
82
|
-
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "lg:hidden m-3 mb-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 tasks menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("nav", { "aria-label": "Tasks", className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto", children: navContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Tasks", children: _jsx("div", { className: "flex flex-col gap-4", children: navContent("mobile") }) })] }));
|
|
82
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "lg:hidden m-3 mb-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 tasks menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("nav", { "aria-label": "Tasks", className: "hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto", children: navContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Tasks", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-4", children: navContent("mobile") }) })] }));
|
|
83
83
|
}
|
|
@@ -11,8 +11,10 @@ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.j
|
|
|
11
11
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
12
|
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
13
13
|
import AppShell from "../../layout/AppShell.js";
|
|
14
|
-
import {
|
|
14
|
+
import { useLocation } from "@rapidrest/react/client";
|
|
15
|
+
import { useNavigate } from "../../../navigation/index.js";
|
|
15
16
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
17
|
+
import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
16
18
|
const TasksShellContext = createContext({ mailboxes: [] });
|
|
17
19
|
/** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
|
|
18
20
|
export function useTasksShell() {
|
|
@@ -35,7 +37,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
35
37
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
36
38
|
// Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
|
|
37
39
|
// effect, not during render, so the server render and the hydrating render agree.
|
|
38
|
-
const search =
|
|
40
|
+
const { search } = useLocation();
|
|
39
41
|
const navigate = useNavigate();
|
|
40
42
|
useEffect(() => {
|
|
41
43
|
setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
|
|
@@ -51,7 +53,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
51
53
|
}
|
|
52
54
|
listMailboxes({ limit: 100 })
|
|
53
55
|
.then((result) => {
|
|
54
|
-
setMailboxes(result);
|
|
56
|
+
setMailboxes(orderMailboxes(result, userUid));
|
|
55
57
|
setStatus("ready");
|
|
56
58
|
})
|
|
57
59
|
.catch((err) => {
|
|
@@ -60,7 +62,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
60
62
|
});
|
|
61
63
|
}, [userUid]);
|
|
62
64
|
const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
|
|
63
|
-
mailboxes
|
|
65
|
+
primaryMailboxUid(mailboxes, userUid);
|
|
64
66
|
useEffect(() => {
|
|
65
67
|
if (!mailboxUid) {
|
|
66
68
|
setFolders([]);
|
|
@@ -98,7 +100,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
98
100
|
}, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), folderError && _jsx(Alert, { children: folderError })] }));
|
|
99
101
|
// With a single mailbox and no error this column would be empty: no 224 px of nothing beside the tasks' own menu.
|
|
100
102
|
const hasSidebar = mailboxes.length > 1 || !!folderError;
|
|
101
|
-
inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") })), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(TasksShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
103
|
+
inner = (_jsxs(_Fragment, { children: [hasSidebar && (_jsx("aside", { className: "hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") })), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", fullScreen: true, children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [hasSidebar && (_jsx("button", { type: "button", className: "lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(TasksShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
102
104
|
}
|
|
103
105
|
return (_jsx(AppShell, { active: "tasks", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
|
|
104
106
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import { accountUrlOf } from "../auth/accountUrl.js";
|
|
6
6
|
import { APP_HREFS, SETTINGS_HREF, isSettingsPath } from "../navigation/appHrefs.js";
|
|
7
|
-
import { useNavigate } from "../navigation/
|
|
7
|
+
import { useNavigate } from "../navigation/index.js";
|
|
8
8
|
import { SHORTCUTS } from "./keymap.js";
|
|
9
9
|
import { useShortcut } from "./useShortcut.js";
|
|
10
10
|
/**
|
|
@@ -46,6 +46,16 @@ export declare function badgeLabel(badge: FolderBadge): string;
|
|
|
46
46
|
export declare function inboxUnreadTotal(mailboxFolders: {
|
|
47
47
|
folders: Folder[];
|
|
48
48
|
}[], counts: Record<string, FolderCount>): number;
|
|
49
|
+
/**
|
|
50
|
+
* The folder types whose unread count is left out of a collapsed sidebar section's heading badge: Drafts and Outbox (their badges
|
|
51
|
+
* count messages held, not unread mail), and Sent Items, Deleted Items and Junk Email (an unread count there is noise, not mail
|
|
52
|
+
* to notice). It matches the folders `badgeFor()` shows no unread badge for; every other type - Inbox, Archive, the user's own
|
|
53
|
+
* folders - counts, so mail that a rule filed out of the Inbox is not overlooked in a hidden section. Change it here to change that.
|
|
54
|
+
*/
|
|
55
|
+
export declare const UNREAD_BADGE_EXCLUDED_FOLDER_TYPES: readonly FolderType[];
|
|
56
|
+
/** The unread messages in the `folders` that carry an unread count (all but `UNREAD_BADGE_EXCLUDED_FOLDER_TYPES`) - what a collapsed
|
|
57
|
+
* sidebar section shows on its heading, so new mail in a hidden section is still noticed. */
|
|
58
|
+
export declare function unreadBadgeTotal(folders: readonly Folder[], counts: Record<string, FolderCount>): number;
|
|
49
59
|
/** How long after the last change of this page's own has finished a server-published count is trusted again: one published
|
|
50
60
|
* about an earlier state of the folder could otherwise arrive late and put back what the change just took away. */
|
|
51
61
|
export declare const COUNT_QUIET_MS = 1500;
|
|
@@ -51,6 +51,18 @@ export function badgeLabel(badge) {
|
|
|
51
51
|
export function inboxUnreadTotal(mailboxFolders, counts) {
|
|
52
52
|
return mailboxFolders.reduce((total, entry) => total + entry.folders.filter((folder) => folder.type === "inbox").reduce((sum, folder) => sum + countOfFolder(folder, counts).unread, 0), 0);
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The folder types whose unread count is left out of a collapsed sidebar section's heading badge: Drafts and Outbox (their badges
|
|
56
|
+
* count messages held, not unread mail), and Sent Items, Deleted Items and Junk Email (an unread count there is noise, not mail
|
|
57
|
+
* to notice). It matches the folders `badgeFor()` shows no unread badge for; every other type - Inbox, Archive, the user's own
|
|
58
|
+
* folders - counts, so mail that a rule filed out of the Inbox is not overlooked in a hidden section. Change it here to change that.
|
|
59
|
+
*/
|
|
60
|
+
export const UNREAD_BADGE_EXCLUDED_FOLDER_TYPES = ["drafts", "outbox", "sent_items", "deleted_items", "junk"];
|
|
61
|
+
/** The unread messages in the `folders` that carry an unread count (all but `UNREAD_BADGE_EXCLUDED_FOLDER_TYPES`) - what a collapsed
|
|
62
|
+
* sidebar section shows on its heading, so new mail in a hidden section is still noticed. */
|
|
63
|
+
export function unreadBadgeTotal(folders, counts) {
|
|
64
|
+
return folders.reduce((total, folder) => (UNREAD_BADGE_EXCLUDED_FOLDER_TYPES.includes(folder.type) ? total : total + countOfFolder(folder, counts).unread), 0);
|
|
65
|
+
}
|
|
54
66
|
/** How long after the last change of this page's own has finished a server-published count is trusted again: one published
|
|
55
67
|
* about an earlier state of the folder could otherwise arrive late and put back what the change just took away. */
|
|
56
68
|
export const COUNT_QUIET_MS = 1500;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Folder } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
2
|
+
/** Forgets every remembered folder - for the tests. */
|
|
3
|
+
export declare function clearResolvedFolders(): void;
|
|
4
|
+
/**
|
|
5
|
+
* A mailbox's own folder of `type` (Junk Email, Deleted Items, Inbox) - the reading pane's counterpart of the mail page's bulk
|
|
6
|
+
* `resolveFolderOfType()`, so Report junk and Delete on one message go where the selection bar's go.
|
|
7
|
+
*
|
|
8
|
+
* Folders belong to a mailbox: only those of `mailboxUid` in `known` count (a message opened from a shared mailbox, or from a search
|
|
9
|
+
* over several, is filed in ITS mailbox's Junk, never the open one's). The server makes every well-known folder itself, so one the tree
|
|
10
|
+
* lacks is asked for - the tree may only be out of date - and used, never created twice; only a server that really has none is asked to
|
|
11
|
+
* make it. `onFound` is told about a folder the tree did not have, so the sidebar picks it up.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveFolderOfType(mailboxUid: string, type: Folder["type"], name: string, known?: Folder[], onFound?: (folder: Folder) => void): Promise<string>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { createFolder, listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
6
|
+
/** The folders this page load has had to look up or make for a mailbox, so a second Report junk does not ask again. */
|
|
7
|
+
const resolved = new Map();
|
|
8
|
+
/** Forgets every remembered folder - for the tests. */
|
|
9
|
+
export function clearResolvedFolders() {
|
|
10
|
+
resolved.clear();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A mailbox's own folder of `type` (Junk Email, Deleted Items, Inbox) - the reading pane's counterpart of the mail page's bulk
|
|
14
|
+
* `resolveFolderOfType()`, so Report junk and Delete on one message go where the selection bar's go.
|
|
15
|
+
*
|
|
16
|
+
* Folders belong to a mailbox: only those of `mailboxUid` in `known` count (a message opened from a shared mailbox, or from a search
|
|
17
|
+
* over several, is filed in ITS mailbox's Junk, never the open one's). The server makes every well-known folder itself, so one the tree
|
|
18
|
+
* lacks is asked for - the tree may only be out of date - and used, never created twice; only a server that really has none is asked to
|
|
19
|
+
* make it. `onFound` is told about a folder the tree did not have, so the sidebar picks it up.
|
|
20
|
+
*/
|
|
21
|
+
export async function resolveFolderOfType(mailboxUid, type, name, known = [], onFound) {
|
|
22
|
+
const inTree = known.find((folder) => folder.mailboxUid === mailboxUid && folder.type === type);
|
|
23
|
+
if (inTree) {
|
|
24
|
+
return inTree.uid;
|
|
25
|
+
}
|
|
26
|
+
const key = `${mailboxUid}:${type}`;
|
|
27
|
+
const remembered = resolved.get(key);
|
|
28
|
+
if (remembered) {
|
|
29
|
+
return remembered;
|
|
30
|
+
}
|
|
31
|
+
const listed = (await listFolders(mailboxUid)).find((folder) => folder.type === type);
|
|
32
|
+
if (listed) {
|
|
33
|
+
onFound?.(listed);
|
|
34
|
+
resolved.set(key, listed.uid);
|
|
35
|
+
return listed.uid;
|
|
36
|
+
}
|
|
37
|
+
const created = await createFolder({ mailboxUid, name, type });
|
|
38
|
+
resolved.set(key, created.uid);
|
|
39
|
+
return created.uid;
|
|
40
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
2
|
+
import type { MailAddressLike } from "@rapidmx/react-shared/mail/mailAddress.js";
|
|
3
|
+
import type { NotificationAction } from "../../notifications/store.js";
|
|
4
|
+
import type { SendRequest } from "./sendJob.js";
|
|
5
|
+
/**
|
|
6
|
+
* A reply (or forward) this tab has sent, as the open conversation shows it until the server's own copy is in the thread.
|
|
7
|
+
*
|
|
8
|
+
* This is a *view* of the background send that is already there, not a second state machine: `startSend()` (`sendJob.ts`) opens an entry, and the
|
|
9
|
+
* places that already report how a send went close it - the client-side failure (`notifyFailure()`), the server's own outcome (`handleSendEvent()`
|
|
10
|
+
* in `sendOutcomes.ts`: `send-succeeded`, `send-failed`) and a synchronous send. The Outbox indicator, the pop-ups and this entry therefore agree,
|
|
11
|
+
* because they are all told by the same code at the same moment.
|
|
12
|
+
*
|
|
13
|
+
* Why the message has to be shown from here for a while: a message the server accepted is in Outbox, which has no `conversationId` yet - it is
|
|
14
|
+
* resolved when the message is relayed and filed in Sent Items (`ScheduledSendJob`), keeping the same `uid`. Until then no fetch of the
|
|
15
|
+
* conversation can return it, so the thread pane draws it from what was composed.
|
|
16
|
+
*
|
|
17
|
+
* `sending` covers everything up to the server's answer (assembling, the request, the queue in Outbox); `failed` is a send that did not go and is
|
|
18
|
+
* the sender's to resolve (`actions` are the very ones the pop-up offers: Retry, Open draft ...); `sent` is a message the server has relayed and
|
|
19
|
+
* that is waiting for the thread to be read again to find its Sent Items copy.
|
|
20
|
+
*/
|
|
21
|
+
export type OutgoingState = "sending" | "failed" | "sent";
|
|
22
|
+
export interface OutgoingReply {
|
|
23
|
+
/** The draft's uid - which is also the uid of the message in Outbox and of its Sent Items copy, and so what the real message replaces this by. */
|
|
24
|
+
uid: string;
|
|
25
|
+
mailboxUid: string;
|
|
26
|
+
/** What the message replies to (`Message.inReplyTo` / `references`): how the pane knows which conversation it belongs to. */
|
|
27
|
+
inReplyTo?: string;
|
|
28
|
+
references: string[];
|
|
29
|
+
sender: MailAddressLike;
|
|
30
|
+
to: MailAddressLike[];
|
|
31
|
+
cc: MailAddressLike[];
|
|
32
|
+
bcc: MailAddressLike[];
|
|
33
|
+
subject: string;
|
|
34
|
+
/** The composed body, as the editor wrote it. */
|
|
35
|
+
html: string;
|
|
36
|
+
startedAt: number;
|
|
37
|
+
state: OutgoingState;
|
|
38
|
+
/** `failed`: why. */
|
|
39
|
+
failure?: string;
|
|
40
|
+
/** `failed`: what the sender can do about it. */
|
|
41
|
+
actions: NotificationAction[];
|
|
42
|
+
/** `sent`: how many times the thread was read without finding the message - see `settleOutgoing()`. */
|
|
43
|
+
misses: number;
|
|
44
|
+
}
|
|
45
|
+
/** A sent message that a read of its conversation has not found this many times is not part of that conversation (the server filed it elsewhere). */
|
|
46
|
+
export declare const MAX_SETTLE_MISSES = 2;
|
|
47
|
+
/**
|
|
48
|
+
* Starts showing `request`'s message in the conversation it replies to - or does nothing for a message that replies to nothing (a new message
|
|
49
|
+
* starts a conversation of its own, it is never part of an open one) and for a send-later message (it is not sent yet). Called again for the same
|
|
50
|
+
* message - a Retry - it starts over as `sending`.
|
|
51
|
+
*
|
|
52
|
+
* The thread is `request.threading` (the compose session's), else what the draft itself records: a draft opened again from "Open draft" is a
|
|
53
|
+
* session with no threading of its own, but the server kept it on the message.
|
|
54
|
+
*/
|
|
55
|
+
export declare function trackOutgoing(request: SendRequest): void;
|
|
56
|
+
/** The send did not go: `failure` is why, `actions` what can be done (Retry, Open draft ...). */
|
|
57
|
+
export declare function failOutgoing(uid: string, failure: string, actions: NotificationAction[]): void;
|
|
58
|
+
/** A failed send is being tried again. */
|
|
59
|
+
export declare function markOutgoingSending(uid: string): void;
|
|
60
|
+
/** The server has relayed the message: its Sent Items copy is what the conversation shows next. */
|
|
61
|
+
export declare function markOutgoingSent(uid: string): void;
|
|
62
|
+
/** Stops showing a message - it went back to Drafts to be edited. */
|
|
63
|
+
export declare function forgetOutgoing(uid: string): void;
|
|
64
|
+
/** Whether `reply` continues the conversation `messages` are: it answers one of them, or its `References` chain runs through one. */
|
|
65
|
+
export declare function belongsToThread(reply: OutgoingReply, messages: readonly Pick<Message, "messageId">[]): boolean;
|
|
66
|
+
/** The uids of the messages sent from here whose real copy is in `messages`, the thread as it was just read. */
|
|
67
|
+
export declare function adoptedOutgoing(mailboxUid: string, messages: readonly Message[]): string[];
|
|
68
|
+
/**
|
|
69
|
+
* Called with a mailbox's conversation as it was just read, `messages` being the whole thread: forgets the messages whose real copy is now in it
|
|
70
|
+
* (`adoptedOutgoing()`), and gives up on a *sent* message that belongs to this thread but that the read did not find `MAX_SETTLE_MISSES` times in a
|
|
71
|
+
* row - the server filed it in a conversation of its own, and it is not to be shown here. A message of another conversation is left alone.
|
|
72
|
+
*
|
|
73
|
+
* A pane draws the messages it has read *before* it calls this, so that there is never a moment with neither the pending card nor the real one.
|
|
74
|
+
*/
|
|
75
|
+
export declare function settleOutgoing(mailboxUid: string, messages: readonly Message[]): void;
|
|
76
|
+
export declare function getOutgoingReplies(): readonly OutgoingReply[];
|
|
77
|
+
/** The messages this tab has sent that a conversation may still need to show, oldest first. */
|
|
78
|
+
export declare function useOutgoingReplies(): readonly OutgoingReply[];
|
|
79
|
+
/** Forgets everything - for a test. */
|
|
80
|
+
export declare function resetOutgoingReplies(): void;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useSyncExternalStore } from "react";
|
|
6
|
+
/** The most entries kept; the oldest go first. Every entry is a message this tab sent, so this is generous. */
|
|
7
|
+
const MAX_TRACKED = 25;
|
|
8
|
+
/** A sent message that a read of its conversation has not found this many times is not part of that conversation (the server filed it elsewhere). */
|
|
9
|
+
export const MAX_SETTLE_MISSES = 2;
|
|
10
|
+
const NO_REPLIES = [];
|
|
11
|
+
let replies = [];
|
|
12
|
+
let snapshot = replies;
|
|
13
|
+
const listeners = new Set();
|
|
14
|
+
function commit(next) {
|
|
15
|
+
replies = next;
|
|
16
|
+
snapshot = next;
|
|
17
|
+
for (const listener of [...listeners]) {
|
|
18
|
+
listener();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function update(uid, patch) {
|
|
22
|
+
if (replies.some((reply) => reply.uid === uid)) {
|
|
23
|
+
commit(replies.map((reply) => (reply.uid === uid ? { ...reply, ...patch } : reply)));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Starts showing `request`'s message in the conversation it replies to - or does nothing for a message that replies to nothing (a new message
|
|
28
|
+
* starts a conversation of its own, it is never part of an open one) and for a send-later message (it is not sent yet). Called again for the same
|
|
29
|
+
* message - a Retry - it starts over as `sending`.
|
|
30
|
+
*
|
|
31
|
+
* The thread is `request.threading` (the compose session's), else what the draft itself records: a draft opened again from "Open draft" is a
|
|
32
|
+
* session with no threading of its own, but the server kept it on the message.
|
|
33
|
+
*/
|
|
34
|
+
export function trackOutgoing(request) {
|
|
35
|
+
const source = request.threading ?? request.draft;
|
|
36
|
+
const references = source.references ?? [];
|
|
37
|
+
if (request.scheduledSendTime || (!source.inReplyTo && references.length === 0)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const { mailbox } = request;
|
|
41
|
+
const reply = {
|
|
42
|
+
uid: request.draft.uid,
|
|
43
|
+
mailboxUid: request.mailboxUid,
|
|
44
|
+
inReplyTo: source.inReplyTo,
|
|
45
|
+
references,
|
|
46
|
+
// The mailbox is only unknown when the window's own load of it had not succeeded: "Me" is what a sender is called then.
|
|
47
|
+
sender: mailbox ? { address: mailbox.primarySmtpAddress, displayName: mailbox.displayName } : { address: "Me" },
|
|
48
|
+
to: request.to,
|
|
49
|
+
cc: request.cc,
|
|
50
|
+
bcc: request.bcc,
|
|
51
|
+
subject: request.subject,
|
|
52
|
+
html: request.html,
|
|
53
|
+
startedAt: Date.now(),
|
|
54
|
+
state: "sending",
|
|
55
|
+
actions: [],
|
|
56
|
+
misses: 0,
|
|
57
|
+
};
|
|
58
|
+
commit([...replies.filter((existing) => existing.uid !== reply.uid), reply].slice(-MAX_TRACKED));
|
|
59
|
+
}
|
|
60
|
+
/** The send did not go: `failure` is why, `actions` what can be done (Retry, Open draft ...). */
|
|
61
|
+
export function failOutgoing(uid, failure, actions) {
|
|
62
|
+
update(uid, { state: "failed", failure, actions });
|
|
63
|
+
}
|
|
64
|
+
/** A failed send is being tried again. */
|
|
65
|
+
export function markOutgoingSending(uid) {
|
|
66
|
+
update(uid, { state: "sending", failure: undefined, actions: [] });
|
|
67
|
+
}
|
|
68
|
+
/** The server has relayed the message: its Sent Items copy is what the conversation shows next. */
|
|
69
|
+
export function markOutgoingSent(uid) {
|
|
70
|
+
update(uid, { state: "sent" });
|
|
71
|
+
}
|
|
72
|
+
/** Stops showing a message - it went back to Drafts to be edited. */
|
|
73
|
+
export function forgetOutgoing(uid) {
|
|
74
|
+
if (replies.some((reply) => reply.uid === uid)) {
|
|
75
|
+
commit(replies.filter((reply) => reply.uid !== uid));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Whether `reply` continues the conversation `messages` are: it answers one of them, or its `References` chain runs through one. */
|
|
79
|
+
export function belongsToThread(reply, messages) {
|
|
80
|
+
return messages.some((message) => !!message.messageId && (message.messageId === reply.inReplyTo || reply.references.includes(message.messageId)));
|
|
81
|
+
}
|
|
82
|
+
/** The uids of the messages sent from here whose real copy is in `messages`, the thread as it was just read. */
|
|
83
|
+
export function adoptedOutgoing(mailboxUid, messages) {
|
|
84
|
+
return replies.filter((reply) => reply.mailboxUid === mailboxUid && messages.some((message) => message.uid === reply.uid)).map((reply) => reply.uid);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Called with a mailbox's conversation as it was just read, `messages` being the whole thread: forgets the messages whose real copy is now in it
|
|
88
|
+
* (`adoptedOutgoing()`), and gives up on a *sent* message that belongs to this thread but that the read did not find `MAX_SETTLE_MISSES` times in a
|
|
89
|
+
* row - the server filed it in a conversation of its own, and it is not to be shown here. A message of another conversation is left alone.
|
|
90
|
+
*
|
|
91
|
+
* A pane draws the messages it has read *before* it calls this, so that there is never a moment with neither the pending card nor the real one.
|
|
92
|
+
*/
|
|
93
|
+
export function settleOutgoing(mailboxUid, messages) {
|
|
94
|
+
const adopted = new Set(adoptedOutgoing(mailboxUid, messages));
|
|
95
|
+
const next = [];
|
|
96
|
+
let changed = adopted.size > 0;
|
|
97
|
+
for (const reply of replies) {
|
|
98
|
+
if (adopted.has(reply.uid)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (reply.mailboxUid === mailboxUid && reply.state === "sent" && belongsToThread(reply, messages)) {
|
|
102
|
+
changed = true;
|
|
103
|
+
if (reply.misses + 1 < MAX_SETTLE_MISSES) {
|
|
104
|
+
next.push({ ...reply, misses: reply.misses + 1 });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
next.push(reply);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (changed) {
|
|
112
|
+
commit(next);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export function getOutgoingReplies() {
|
|
116
|
+
return snapshot;
|
|
117
|
+
}
|
|
118
|
+
function subscribe(listener) {
|
|
119
|
+
listeners.add(listener);
|
|
120
|
+
return () => {
|
|
121
|
+
listeners.delete(listener);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** The messages this tab has sent that a conversation may still need to show, oldest first. */
|
|
125
|
+
export function useOutgoingReplies() {
|
|
126
|
+
return useSyncExternalStore(subscribe, getOutgoingReplies, () => NO_REPLIES);
|
|
127
|
+
}
|
|
128
|
+
/** Forgets everything - for a test. */
|
|
129
|
+
export function resetOutgoingReplies() {
|
|
130
|
+
commit([]);
|
|
131
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Attachment, ComposeRecipientInput, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
1
|
+
import { Attachment, ComposeRecipientInput, DraftThreading, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
2
2
|
import { RecipientEncryptionStatus } from "@rapidmx/react-shared/crypto/composeSecurity.js";
|
|
3
3
|
import { EncryptionPolicy } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
|
|
4
4
|
import { SendDecisionInput } from "./sendDecision.js";
|
|
@@ -34,6 +34,10 @@ export interface SendRequest {
|
|
|
34
34
|
forcePlaintext: boolean;
|
|
35
35
|
/** Settles when the window's own autosaves have landed, so none can overwrite the assembly below. */
|
|
36
36
|
saved?: Promise<unknown>;
|
|
37
|
+
/** The thread this message continues (a reply or forward), as the compose session was opened with it - what lets the conversation on screen
|
|
38
|
+
* show the message as soon as it is sent (see `outgoingReplies.ts`). Absent for a new message, and for a draft opened again, which the draft's own
|
|
39
|
+
* `inReplyTo`/`references` stand in for. */
|
|
40
|
+
threading?: DraftThreading;
|
|
37
41
|
}
|
|
38
42
|
/** How long a background send waits for the mailbox / encryption policy it did not have, before it goes ahead treating them as unknown. Nobody waits for it. */
|
|
39
43
|
export declare const CONTEXT_GRACE_MS = 4000;
|
|
@@ -13,6 +13,7 @@ import { policyCanAutoEncrypt } from "../../components/mail/compose/encryptionRe
|
|
|
13
13
|
import { dismiss, notify } from "../../notifications/store.js";
|
|
14
14
|
import { INLINE_IMAGE_PATTERN, decideSend } from "./sendDecision.js";
|
|
15
15
|
import { beginPendingSend, finishPendingSend, setPendingStage } from "./pendingSends.js";
|
|
16
|
+
import { failOutgoing, forgetOutgoing, markOutgoingSending, markOutgoingSent, trackOutgoing } from "./outgoingReplies.js";
|
|
16
17
|
import { openComposeFromOutside, requestUnlockFromOutside } from "./composeBridge.js";
|
|
17
18
|
import { sendState } from "./sendState.js";
|
|
18
19
|
/** How long a background send waits for the mailbox / encryption policy it did not have, before it goes ahead treating them as unknown. Nobody waits for it. */
|
|
@@ -209,7 +210,7 @@ export function describeMessage(subject, recipients) {
|
|
|
209
210
|
}
|
|
210
211
|
/** Re-opens the compose window around a request's message, with everything as it was typed. */
|
|
211
212
|
export function openDraftFromRequest(request) {
|
|
212
|
-
|
|
213
|
+
const opened = openComposeFromOutside({
|
|
213
214
|
mailboxUid: request.mailboxUid,
|
|
214
215
|
resume: {
|
|
215
216
|
draft: request.draft,
|
|
@@ -225,6 +226,11 @@ export function openDraftFromRequest(request) {
|
|
|
225
226
|
encryptRequested: request.encryptRequested,
|
|
226
227
|
},
|
|
227
228
|
});
|
|
229
|
+
// Being edited again, it is no longer a message on its way in the conversation.
|
|
230
|
+
if (opened) {
|
|
231
|
+
forgetOutgoing(request.draft.uid);
|
|
232
|
+
}
|
|
233
|
+
return opened;
|
|
228
234
|
}
|
|
229
235
|
function failureNotificationId(draftUid) {
|
|
230
236
|
return `send-failed:${draftUid}`;
|
|
@@ -257,6 +263,8 @@ function notifyFailure(request, failure) {
|
|
|
257
263
|
details: failure.details,
|
|
258
264
|
actions,
|
|
259
265
|
});
|
|
266
|
+
// The conversation on screen shows the same failure under the message, with the same ways out.
|
|
267
|
+
failOutgoing(uid, failure.message, actions);
|
|
260
268
|
}
|
|
261
269
|
const SENT_BURST_MS = 6000;
|
|
262
270
|
/** The short "Message sent" confirmation - several sent close together are one pop-up counting up, never a stack of them. */
|
|
@@ -280,6 +288,8 @@ export function startSend(request) {
|
|
|
280
288
|
}
|
|
281
289
|
// The failure of an earlier attempt is resolved by trying again.
|
|
282
290
|
dismiss(failureNotificationId(uid));
|
|
291
|
+
// A reply or forward is on screen in its conversation from this moment, before any of the work below.
|
|
292
|
+
trackOutgoing(request);
|
|
283
293
|
const tracker = sendState.countTracker?.(request.mailboxUid);
|
|
284
294
|
void attempt(request).then((outcome) => {
|
|
285
295
|
finishPendingSend(uid);
|
|
@@ -294,6 +304,8 @@ export function startSend(request) {
|
|
|
294
304
|
sendState.queuedListener?.(request.mailboxUid);
|
|
295
305
|
}
|
|
296
306
|
else if (outcome.kind === "sent") {
|
|
307
|
+
// A server with no queue relayed it before answering: the Sent Items copy exists already.
|
|
308
|
+
markOutgoingSent(uid);
|
|
297
309
|
notifySent();
|
|
298
310
|
}
|
|
299
311
|
else {
|
|
@@ -312,6 +324,7 @@ export function startSend(request) {
|
|
|
312
324
|
export async function retrySend(request) {
|
|
313
325
|
if (await alreadyAccepted(request)) {
|
|
314
326
|
dismiss(failureNotificationId(request.draft.uid));
|
|
327
|
+
markOutgoingSending(request.draft.uid);
|
|
315
328
|
notify({ id: `already-sent:${request.draft.uid}`, kind: "info", title: "This message is already on its way", timeoutMs: 5000 });
|
|
316
329
|
return;
|
|
317
330
|
}
|
|
@@ -9,6 +9,7 @@ import { loadOriginalMessage } from "../../components/mail/compose/quotedBody.js
|
|
|
9
9
|
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
10
10
|
import { dismiss, notify } from "../../notifications/store.js";
|
|
11
11
|
import { beginPendingSend, finishPendingSend, setPendingStage } from "./pendingSends.js";
|
|
12
|
+
import { failOutgoing, forgetOutgoing, markOutgoingSending, markOutgoingSent } from "./outgoingReplies.js";
|
|
12
13
|
import { openComposeFromOutside } from "./composeBridge.js";
|
|
13
14
|
import { describeMessage, forgetRetainedRequest, notifySent, openDraftFromRequest, retainedRequest } from "./sendJob.js";
|
|
14
15
|
/**
|
|
@@ -39,6 +40,8 @@ function escapeHtml(text) {
|
|
|
39
40
|
async function openFailedDraft(event) {
|
|
40
41
|
try {
|
|
41
42
|
const moved = await moveBackToDrafts(event.uid, event.mailboxUid ?? "");
|
|
43
|
+
// Back in Drafts it is no longer a message on its way in its conversation.
|
|
44
|
+
forgetOutgoing(event.uid);
|
|
42
45
|
const request = retainedRequest(event.uid);
|
|
43
46
|
if (request) {
|
|
44
47
|
openDraftFromRequest({ ...request, draft: moved });
|
|
@@ -84,6 +87,7 @@ async function retryFailed(event) {
|
|
|
84
87
|
return;
|
|
85
88
|
}
|
|
86
89
|
dismiss(failedId(event.uid));
|
|
90
|
+
markOutgoingSending(event.uid);
|
|
87
91
|
try {
|
|
88
92
|
setPendingStage(event.uid, "queuing");
|
|
89
93
|
const result = await queueMessageSend(event.uid);
|
|
@@ -103,6 +107,8 @@ export function handleSendEvent(event) {
|
|
|
103
107
|
if (event.action === "send-succeeded") {
|
|
104
108
|
dismiss(retryId(event.uid));
|
|
105
109
|
dismiss(failedId(event.uid));
|
|
110
|
+
// The conversation on screen swaps the message it drew for the Sent Items copy (a message this tab did not send has nothing drawn).
|
|
111
|
+
markOutgoingSent(event.uid);
|
|
106
112
|
// The server reports every message it relays, scheduled ones from long ago included: only a message this tab queued in the background is
|
|
107
113
|
// worth a confirmation (several are one pop-up counting up). Anything else - a send-later message going out, another tab's send - is quiet.
|
|
108
114
|
if (retainedRequest(event.uid)) {
|
|
@@ -127,15 +133,18 @@ export function handleSendEvent(event) {
|
|
|
127
133
|
return;
|
|
128
134
|
}
|
|
129
135
|
dismiss(retryId(event.uid));
|
|
136
|
+
const actions = [
|
|
137
|
+
{ label: "Retry", onClick: () => void retryFailed(event) },
|
|
138
|
+
{ label: "Open draft", onClick: () => void openFailedDraft(event) },
|
|
139
|
+
];
|
|
130
140
|
notify({
|
|
131
141
|
id: failedId(event.uid),
|
|
132
142
|
kind: "error",
|
|
133
143
|
title: "This message wasn't sent",
|
|
134
144
|
message: `${what}: ${failure.message}`,
|
|
135
145
|
details: failure.lines,
|
|
136
|
-
actions
|
|
137
|
-
{ label: "Retry", onClick: () => void retryFailed(event) },
|
|
138
|
-
{ label: "Open draft", onClick: () => void openFailedDraft(event) },
|
|
139
|
-
],
|
|
146
|
+
actions,
|
|
140
147
|
});
|
|
148
|
+
// The conversation on screen shows the same failure under the message, with the same ways out.
|
|
149
|
+
failOutgoing(event.uid, failure.message, actions);
|
|
141
150
|
}
|