@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
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
|
5
|
+
import React, { createContext, PropsWithChildren, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
6
6
|
import { HiOutlineBars3, HiOutlinePencilSquare } from "react-icons/hi2";
|
|
7
7
|
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
8
8
|
import { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.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, { AppShellProps } 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,15 +19,19 @@ import LocalIndexLifecycle from "../../../search/LocalIndexLifecycle.js";
|
|
|
17
19
|
import { LiveUpdates, 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";
|
|
22
|
+
import { primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
20
23
|
import {
|
|
21
24
|
CountTracker,
|
|
22
25
|
FolderBadge,
|
|
23
26
|
FolderCount,
|
|
24
27
|
badgeFor,
|
|
25
|
-
badgeLabel,
|
|
26
28
|
countOfFolder,
|
|
27
29
|
inboxUnreadTotal,
|
|
30
|
+
unreadBadgeTotal,
|
|
28
31
|
} from "../../../mail/folderCounts.js";
|
|
32
|
+
import { ALL_MAILBOXES_SECTION, useSidebarSections } from "../../../mail/useCollapsedSections.js";
|
|
33
|
+
import FolderBadgeChip from "./FolderBadgeChip.js";
|
|
34
|
+
import SidebarSection from "./SidebarSection.js";
|
|
29
35
|
import { useUnreadTitle } from "../../../mail/useUnreadTitle.js";
|
|
30
36
|
import { pendingCountFor, usePendingSends } from "../../../mail/outbox/pendingSends.js";
|
|
31
37
|
import OutboxBadge from "../OutboxBadge.js";
|
|
@@ -96,6 +102,13 @@ export interface MailShellContextValue {
|
|
|
96
102
|
* context value, which is only ever read outside a real shell.
|
|
97
103
|
*/
|
|
98
104
|
trackMessageChange: (previous: Message, next: Message | null) => CountTracker;
|
|
105
|
+
/**
|
|
106
|
+
* Reads every mailbox's real folder counts back and shows them (after `delayMs`, 600 ms by default) - for a change too big to track message by
|
|
107
|
+
* message, such as emptying a folder. A no-op on the default context value, which is only ever read outside a real shell.
|
|
108
|
+
*/
|
|
109
|
+
refreshFolderCounts: (delayMs?: number) => void;
|
|
110
|
+
/** A folder's counts as the sidebar shows them: what the shell has worked out or been told since it loaded, else what the folder was listed with. */
|
|
111
|
+
folderCountOf: (folder: Folder) => FolderCount;
|
|
99
112
|
/**
|
|
100
113
|
* The phone layout's header row beside the folders button, for the page to put its search box in (`createPortal()`): the shell
|
|
101
114
|
* owns the row, the page owns the search state. `null` until the row has rendered, and on the default context value.
|
|
@@ -112,6 +125,8 @@ const MailShellContext = createContext<MailShellContextValue>({
|
|
|
112
125
|
noteFolderUids: () => undefined,
|
|
113
126
|
live: NO_LIVE_UPDATES,
|
|
114
127
|
trackMessageChange: () => NO_TRACKER,
|
|
128
|
+
refreshFolderCounts: () => undefined,
|
|
129
|
+
folderCountOf: (folder) => ({ unread: folder.unreadCount, total: folder.totalCount }),
|
|
115
130
|
mobileSearchSlot: null,
|
|
116
131
|
});
|
|
117
132
|
|
|
@@ -143,21 +158,6 @@ function aggregateBadge(mailboxFolders: MailboxFolders[], type: AggregateFolderT
|
|
|
143
158
|
return badgeFor(type, sum);
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
/** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
|
|
147
|
-
function FolderBadgeChip({ badge }: { badge: FolderBadge }) {
|
|
148
|
-
return (
|
|
149
|
-
<span
|
|
150
|
-
className={[
|
|
151
|
-
"text-xs rounded-pill py-0.5 px-1.5",
|
|
152
|
-
badge.kind === "unread" ? "font-bold bg-primary/15 text-primary-dark" : "font-medium text-text-muted",
|
|
153
|
-
].join(" ")}
|
|
154
|
-
>
|
|
155
|
-
<span aria-hidden="true">{badge.value}</span>
|
|
156
|
-
<span className="sr-only"> {badgeLabel(badge)}</span>
|
|
157
|
-
</span>
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
161
|
type Status = "checking" | "error" | "ready";
|
|
162
162
|
|
|
163
163
|
export { MAILBOX_LIST_LIMIT };
|
|
@@ -200,16 +200,13 @@ function ComposeButton({ mailboxUid }: { mailboxUid?: string }) {
|
|
|
200
200
|
function MobileComposeButton({ mailboxUid }: { mailboxUid?: string }) {
|
|
201
201
|
const { openCompose } = useCompose();
|
|
202
202
|
return (
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
203
|
+
<FloatingActionButton
|
|
204
|
+
label="New message"
|
|
205
|
+
icon={HiOutlinePencilSquare}
|
|
206
206
|
onClick={() => openCompose({ mailboxUid })}
|
|
207
207
|
onPointerEnter={prefetchComposeWindow}
|
|
208
208
|
onFocus={prefetchComposeWindow}
|
|
209
|
-
|
|
210
|
-
>
|
|
211
|
-
<HiOutlinePencilSquare size={22} aria-hidden="true" />
|
|
212
|
-
</button>
|
|
209
|
+
/>
|
|
213
210
|
);
|
|
214
211
|
}
|
|
215
212
|
|
|
@@ -227,18 +224,20 @@ function MailShortcuts({ mailboxUid }: { mailboxUid?: string }) {
|
|
|
227
224
|
/**
|
|
228
225
|
* Mail's own contextual sidebar (every accessible mailbox's own folder tree, plus a merged "All Mailboxes"
|
|
229
226
|
* aggregate section) + content area, rendered inside the shared `AppShell` chrome (icon rail, header,
|
|
230
|
-
* impersonation banner — see that component).
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* `apps/admin/quarantine`'s `readMailboxUid()` — so the server-rendered and just-hydrated client markup
|
|
235
|
-
* match).
|
|
227
|
+
* impersonation banner — see that component). The selected mailbox/folder (or aggregate pseudo-folder) live
|
|
228
|
+
* in the URL's `?mailboxUid=`/`?folderUid=`/`?aggregate=` query params, read from the router's location
|
|
229
|
+
* (`@rapidrest/react`), which is the request's on the server and while hydrating and follows a shallow
|
|
230
|
+
* navigation (a folder link) without remounting the page.
|
|
236
231
|
*
|
|
237
232
|
* Every accessible mailbox's folder tree renders at once - there is no "switch mailbox" affordance
|
|
238
233
|
* anymore (the mailbox `<select>` this shell used to have is gone) - so a single-mailbox user sees zero
|
|
239
234
|
* behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
|
|
240
235
|
* aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
|
|
241
236
|
* of folders" requirement.
|
|
237
|
+
*
|
|
238
|
+
* An address that names nothing opens "All Mailboxes > Inbox" when there is more than one mailbox (that is when the section exists), and
|
|
239
|
+
* 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
|
|
240
|
+
* primary mailbox.
|
|
242
241
|
*/
|
|
243
242
|
export default function MailShell({
|
|
244
243
|
userUid,
|
|
@@ -259,20 +258,16 @@ export default function MailShell({
|
|
|
259
258
|
const { status, error, mailboxes, mailboxFolders, foldersLoading, onFolderCreated, noteFolderUids, live, folderCounts, outbox: outboxStatuses } = hosted ?? own;
|
|
260
259
|
// The messages this tab is still handing to the server: the Outbox pill counts them at once.
|
|
261
260
|
const pendingSends = usePendingSends();
|
|
262
|
-
const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
|
|
263
|
-
const [requestedFolderUid, setRequestedFolderUid] = useState<string | null>(null);
|
|
264
|
-
const [requestedAggregateType, setRequestedAggregateType] = useState<string | null>(null);
|
|
265
261
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
266
262
|
|
|
267
|
-
// The selection lives in the URL, and the router (`
|
|
268
|
-
// clicked - so it is read from the router's location, which updates,
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
263
|
+
// The selection lives in the URL, and the router (`@rapidrest/react`) changes the URL without a page load when a folder link is
|
|
264
|
+
// clicked - a shallow navigation, which keeps this page and its state - so it is read from the router's location, which updates,
|
|
265
|
+
// rather than once from `window.location`. The location is the request's on the server and while hydrating (its `#fragment` is not
|
|
266
|
+
// 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.
|
|
267
|
+
const { search } = useLocation();
|
|
268
|
+
const { requestedMailboxUid, requestedFolderUid, requestedAggregateType } = useMemo(() => {
|
|
272
269
|
const params = new URLSearchParams(search);
|
|
273
|
-
|
|
274
|
-
setRequestedFolderUid(params.get("folderUid"));
|
|
275
|
-
setRequestedAggregateType(params.get("aggregate"));
|
|
270
|
+
return { requestedMailboxUid: params.get("mailboxUid"), requestedFolderUid: params.get("folderUid"), requestedAggregateType: params.get("aggregate") };
|
|
276
271
|
}, [search]);
|
|
277
272
|
// Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
|
|
278
273
|
// with the page - is closed here.
|
|
@@ -280,14 +275,20 @@ export default function MailShell({
|
|
|
280
275
|
setDrawerOpen(false);
|
|
281
276
|
}, [search]);
|
|
282
277
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
278
|
+
// What the address asks for.
|
|
279
|
+
const requestedAggregate: AggregateFolderType | undefined = isAggregateFolderType(requestedAggregateType) ? requestedAggregateType : undefined;
|
|
280
|
+
const requestedMailbox: string | undefined =
|
|
281
|
+
requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined;
|
|
286
282
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
283
|
+
// The mailbox the caller has not chosen: their own, never a shared one that merely sorts first. It is what compose, the new-message
|
|
284
|
+
// shortcut and everything else that needs a single mailbox use while an all-mailboxes view (which has none) is open.
|
|
285
|
+
const defaultMailboxUid = primaryMailboxUid(mailboxes, userUid);
|
|
286
|
+
|
|
287
|
+
// With several mailboxes the address that names nothing opens "All Mailboxes > Inbox", the merged Inbox; with one there is no such
|
|
288
|
+
// section, so it opens that mailbox's own Inbox. A mailbox, a folder or an all-mailboxes view in the address wins over either.
|
|
289
|
+
const aggregateByDefault = !requestedAggregate && !requestedMailbox && requestedFolderUid === null && mailboxes.length > 1;
|
|
290
|
+
const aggregateFolderType: AggregateFolderType | undefined = requestedAggregate ?? (aggregateByDefault ? "inbox" : undefined);
|
|
291
|
+
const mailboxUid: string | undefined = aggregateFolderType ? undefined : (requestedMailbox ?? defaultMailboxUid);
|
|
291
292
|
|
|
292
293
|
const selectedMailboxFolders = mailboxFolders.find((mf) => mf.mailbox.uid === mailboxUid)?.folders ?? [];
|
|
293
294
|
const folderUid: string | undefined = aggregateFolderType
|
|
@@ -296,23 +297,33 @@ export default function MailShell({
|
|
|
296
297
|
selectedMailboxFolders.find((f) => f.type === "inbox")?.uid;
|
|
297
298
|
|
|
298
299
|
// The mailbox `KeyEnrollmentGate`/`LocalIndexLifecycle`/`ComposeButton` treat as "the" mailbox when
|
|
299
|
-
// there's no single selected one to use (aggregate mode) - the caller's own
|
|
300
|
-
// have one, else whichever accessible mailbox happens to be first. See MailShell's own doc comment on
|
|
300
|
+
// there's no single selected one to use (aggregate mode) - the caller's primary mailbox (`primaryMailbox()`). See MailShell's own doc comment on
|
|
301
301
|
// this being an accepted limitation: an aggregate-view message from a *different*, not-yet-visited
|
|
302
302
|
// mailbox may still need that mailbox's own folder view opened directly to unlock/decrypt it.
|
|
303
|
-
const defaultMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
|
|
304
303
|
const activeMailboxUid = mailboxUid ?? defaultMailboxUid;
|
|
305
304
|
|
|
305
|
+
// Which sidebar sections are open: "All mailboxes" and the primary mailbox unless the reader chose otherwise. Any of them can be collapsed;
|
|
306
|
+
// a change of the open folder into another section opens that one (see `useSidebarSections()`); the first selection, once the address has been read
|
|
307
|
+
// 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.
|
|
308
|
+
const sections = useSidebarSections({
|
|
309
|
+
userUid,
|
|
310
|
+
primaryUid: defaultMailboxUid,
|
|
311
|
+
activeId: aggregateFolderType ? ALL_MAILBOXES_SECTION : mailboxUid,
|
|
312
|
+
});
|
|
313
|
+
const collapsible = mailboxes.length > 1;
|
|
314
|
+
|
|
306
315
|
const counts = folderCounts.counts;
|
|
307
316
|
const trackMessageChange = folderCounts.track;
|
|
317
|
+
const refreshFolderCounts = folderCounts.refresh;
|
|
318
|
+
const folderCountOf = useCallback((folder: Folder) => countOfFolder(folder, counts), [counts]);
|
|
308
319
|
// `(3) Acme: Mail` in the tab strip while there is unread mail in an Inbox - kept by the frame when there is one.
|
|
309
320
|
useUnreadTitle(inboxUnreadTotal(mailboxFolders, counts), { enabled: !hosted });
|
|
310
321
|
|
|
311
322
|
// 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.
|
|
312
323
|
const [mobileSearchSlot, setMobileSearchSlot] = useState<HTMLElement | null>(null);
|
|
313
324
|
const contextValue = useMemo<MailShellContextValue>(
|
|
314
|
-
() => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, mobileSearchSlot }),
|
|
315
|
-
[mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, mobileSearchSlot],
|
|
325
|
+
() => ({ mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, refreshFolderCounts, folderCountOf, mobileSearchSlot }),
|
|
326
|
+
[mailboxUid, folderUid, aggregateFolderType, mailboxes, mailboxFolders, onFolderCreated, noteFolderUids, live, trackMessageChange, refreshFolderCounts, folderCountOf, mobileSearchSlot],
|
|
316
327
|
);
|
|
317
328
|
|
|
318
329
|
// A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else for a
|
|
@@ -345,7 +356,8 @@ export default function MailShell({
|
|
|
345
356
|
// A function, not a plain JSX constant — it's rendered twice (desktop `<aside>` + mobile
|
|
346
357
|
// `Drawer`), possibly *simultaneously* mounted (the aside is only CSS-hidden below `md`, not
|
|
347
358
|
// unmounted).
|
|
348
|
-
|
|
359
|
+
// `idPrefix` keeps each section's folder list's id (which its heading's `aria-controls` names) unique between the two.
|
|
360
|
+
const sidebarContent = (idPrefix: string) => (
|
|
349
361
|
<>
|
|
350
362
|
<div className="p-3">
|
|
351
363
|
<ComposeButton mailboxUid={defaultMailboxUid} />
|
|
@@ -357,91 +369,106 @@ export default function MailShell({
|
|
|
357
369
|
) : (
|
|
358
370
|
<nav className="flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3">
|
|
359
371
|
{mailboxes.length > 1 && (
|
|
360
|
-
<
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
className={[
|
|
372
|
-
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
373
|
-
aggregateFolderType === type
|
|
374
|
-
? "bg-primary/10 text-primary-dark font-semibold"
|
|
375
|
-
: "text-text hover:bg-surface-alt",
|
|
376
|
-
].join(" ")}
|
|
377
|
-
>
|
|
378
|
-
<span className={badge?.kind === "unread" ? "font-semibold" : undefined}>{FOLDER_LABELS[type]}</span>
|
|
379
|
-
{badge && <FolderBadgeChip badge={badge} />}
|
|
380
|
-
</a>
|
|
381
|
-
);
|
|
382
|
-
})}
|
|
383
|
-
</div>
|
|
384
|
-
</div>
|
|
385
|
-
)}
|
|
386
|
-
{mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (
|
|
387
|
-
<div key={mailbox.uid}>
|
|
388
|
-
<div className="text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate">
|
|
389
|
-
{mailbox.displayName}
|
|
390
|
-
{mailbox.ownerUserUid ? "" : " (shared)"}
|
|
391
|
-
</div>
|
|
392
|
-
{mailboxError && (
|
|
393
|
-
<div className="px-2.5 pb-1">
|
|
394
|
-
<Alert>{mailboxError}</Alert>
|
|
395
|
-
</div>
|
|
396
|
-
)}
|
|
397
|
-
<div className="flex flex-col gap-0.5">
|
|
398
|
-
{folderRows(folders, pendingCountFor(mailbox.uid, pendingSends)).map((row) => {
|
|
399
|
-
if (row.kind === "placeholder") {
|
|
400
|
-
// A folder the server has not made (or told us about) yet - the Outbox and Sent Items are made with the first message
|
|
401
|
-
// sent - shown the moment a message is on its way. Nothing to open; the real folder replaces it when it arrives.
|
|
402
|
-
const sending = pendingCountFor(mailbox.uid, pendingSends);
|
|
403
|
-
return (
|
|
404
|
-
<div
|
|
405
|
-
key={`placeholder-${row.type}`}
|
|
406
|
-
data-folder-placeholder={row.type}
|
|
407
|
-
className="flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5 text-text-muted"
|
|
408
|
-
>
|
|
409
|
-
<span>{FOLDER_LABELS[row.type]}</span>
|
|
410
|
-
{row.type === "outbox" && <OutboxBadge total={sending} pendingHere={sending} />}
|
|
411
|
-
</div>
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
const folder = row.folder;
|
|
415
|
-
const badge = badgeFor(folder.type, countOfFolder(folder, counts));
|
|
416
|
-
return (
|
|
372
|
+
<SidebarSection
|
|
373
|
+
domId={`${idPrefix}-mail-section-${ALL_MAILBOXES_SECTION}`}
|
|
374
|
+
label="All Mailboxes"
|
|
375
|
+
collapsible={collapsible}
|
|
376
|
+
expanded={sections.isExpanded(ALL_MAILBOXES_SECTION)}
|
|
377
|
+
onToggle={() => sections.toggle(ALL_MAILBOXES_SECTION)}
|
|
378
|
+
unread={unreadBadgeTotal(mailboxFolders.flatMap((mf) => mf.folders), counts)}
|
|
379
|
+
>
|
|
380
|
+
{AGGREGATE_FOLDER_TYPES.map((type) => {
|
|
381
|
+
const badge = aggregateBadge(mailboxFolders, type, counts);
|
|
382
|
+
return (
|
|
417
383
|
<a
|
|
418
|
-
key={
|
|
419
|
-
href={`/?
|
|
384
|
+
key={type}
|
|
385
|
+
href={`/?aggregate=${encodeURIComponent(type)}`}
|
|
386
|
+
data-router-shallow=""
|
|
387
|
+
data-router-prefetch="false"
|
|
420
388
|
className={[
|
|
421
389
|
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
422
|
-
|
|
390
|
+
aggregateFolderType === type
|
|
423
391
|
? "bg-primary/10 text-primary-dark font-semibold"
|
|
424
392
|
: "text-text hover:bg-surface-alt",
|
|
425
393
|
].join(" ")}
|
|
426
394
|
>
|
|
427
|
-
<span className={badge?.kind === "unread" ? "font-semibold" : undefined}>
|
|
428
|
-
|
|
429
|
-
</span>
|
|
430
|
-
{badge &&
|
|
431
|
-
(folder.type === "outbox" ? (
|
|
432
|
-
<OutboxBadge
|
|
433
|
-
total={badge.value}
|
|
434
|
-
status={outboxStatuses[folder.uid]}
|
|
435
|
-
pendingHere={pendingCountFor(mailbox.uid, pendingSends)}
|
|
436
|
-
/>
|
|
437
|
-
) : (
|
|
438
|
-
<FolderBadgeChip badge={badge} />
|
|
439
|
-
))}
|
|
395
|
+
<span className={badge?.kind === "unread" ? "font-semibold" : undefined}>{FOLDER_LABELS[type]}</span>
|
|
396
|
+
{badge && <FolderBadgeChip badge={badge} />}
|
|
440
397
|
</a>
|
|
398
|
+
);
|
|
399
|
+
})}
|
|
400
|
+
</SidebarSection>
|
|
401
|
+
)}
|
|
402
|
+
{mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (
|
|
403
|
+
<SidebarSection
|
|
404
|
+
key={mailbox.uid}
|
|
405
|
+
domId={`${idPrefix}-mail-section-${mailbox.uid}`}
|
|
406
|
+
label={
|
|
407
|
+
<>
|
|
408
|
+
{mailbox.displayName}
|
|
409
|
+
{mailbox.ownerUserUid ? "" : " (shared)"}
|
|
410
|
+
</>
|
|
411
|
+
}
|
|
412
|
+
collapsible={collapsible}
|
|
413
|
+
expanded={sections.isExpanded(mailbox.uid)}
|
|
414
|
+
onToggle={() => sections.toggle(mailbox.uid)}
|
|
415
|
+
unread={unreadBadgeTotal(folders, counts)}
|
|
416
|
+
notice={
|
|
417
|
+
mailboxError && (
|
|
418
|
+
<div className="px-2.5 pb-1">
|
|
419
|
+
<Alert>{mailboxError}</Alert>
|
|
420
|
+
</div>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
>
|
|
424
|
+
{folderRows(folders, pendingCountFor(mailbox.uid, pendingSends)).map((row) => {
|
|
425
|
+
if (row.kind === "placeholder") {
|
|
426
|
+
// A folder the server has not made (or told us about) yet - the Outbox and Sent Items are made with the first message
|
|
427
|
+
// sent - shown the moment a message is on its way. Nothing to open; the real folder replaces it when it arrives.
|
|
428
|
+
const sending = pendingCountFor(mailbox.uid, pendingSends);
|
|
429
|
+
return (
|
|
430
|
+
<div
|
|
431
|
+
key={`placeholder-${row.type}`}
|
|
432
|
+
data-folder-placeholder={row.type}
|
|
433
|
+
className="flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5 text-text-muted"
|
|
434
|
+
>
|
|
435
|
+
<span>{FOLDER_LABELS[row.type]}</span>
|
|
436
|
+
{row.type === "outbox" && <OutboxBadge total={sending} pendingHere={sending} />}
|
|
437
|
+
</div>
|
|
441
438
|
);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
|
|
439
|
+
}
|
|
440
|
+
const folder = row.folder;
|
|
441
|
+
const badge = badgeFor(folder.type, countOfFolder(folder, counts));
|
|
442
|
+
return (
|
|
443
|
+
<a
|
|
444
|
+
key={folder.uid}
|
|
445
|
+
href={`/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`}
|
|
446
|
+
data-router-shallow=""
|
|
447
|
+
data-router-prefetch="false"
|
|
448
|
+
className={[
|
|
449
|
+
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
450
|
+
folder.uid === folderUid
|
|
451
|
+
? "bg-primary/10 text-primary-dark font-semibold"
|
|
452
|
+
: "text-text hover:bg-surface-alt",
|
|
453
|
+
].join(" ")}
|
|
454
|
+
>
|
|
455
|
+
<span className={badge?.kind === "unread" ? "font-semibold" : undefined}>
|
|
456
|
+
{FOLDER_LABELS[folder.type] ?? folder.name}
|
|
457
|
+
</span>
|
|
458
|
+
{badge &&
|
|
459
|
+
(folder.type === "outbox" ? (
|
|
460
|
+
<OutboxBadge
|
|
461
|
+
total={badge.value}
|
|
462
|
+
status={outboxStatuses[folder.uid]}
|
|
463
|
+
pendingHere={pendingCountFor(mailbox.uid, pendingSends)}
|
|
464
|
+
/>
|
|
465
|
+
) : (
|
|
466
|
+
<FolderBadgeChip badge={badge} />
|
|
467
|
+
))}
|
|
468
|
+
</a>
|
|
469
|
+
);
|
|
470
|
+
})}
|
|
471
|
+
</SidebarSection>
|
|
445
472
|
))}
|
|
446
473
|
</nav>
|
|
447
474
|
)}
|
|
@@ -451,9 +478,9 @@ export default function MailShell({
|
|
|
451
478
|
inner = (
|
|
452
479
|
<>
|
|
453
480
|
<MailShortcuts mailboxUid={defaultMailboxUid} />
|
|
454
|
-
<aside className="hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col">{sidebarContent()}</aside>
|
|
455
|
-
<Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Folders">
|
|
456
|
-
<div className="flex flex-col">{sidebarContent()}</div>
|
|
481
|
+
<aside className="hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col">{sidebarContent("desktop")}</aside>
|
|
482
|
+
<Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Folders" fullScreen>
|
|
483
|
+
<div className="flex flex-col">{sidebarContent("mobile")}</div>
|
|
457
484
|
</Drawer>
|
|
458
485
|
<main className="flex-1 min-w-0 overflow-y-auto">
|
|
459
486
|
<div className="md:hidden flex items-center gap-2 p-3">
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { ReactNode } from "react";
|
|
6
|
+
import { HiChevronDown } from "react-icons/hi2";
|
|
7
|
+
import FolderBadgeChip from "./FolderBadgeChip.js";
|
|
8
|
+
|
|
9
|
+
export interface SidebarSectionProps {
|
|
10
|
+
/** 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. */
|
|
11
|
+
domId: string;
|
|
12
|
+
/** The heading's text. */
|
|
13
|
+
label: ReactNode;
|
|
14
|
+
/** Whether the heading is a toggle at all. Not for a lone mailbox, whose section has nothing to be hidden for. */
|
|
15
|
+
collapsible: boolean;
|
|
16
|
+
expanded: boolean;
|
|
17
|
+
onToggle: () => void;
|
|
18
|
+
/** 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. */
|
|
19
|
+
unread?: number;
|
|
20
|
+
/** Shown between the heading and the folders, collapsed or not: a failure to load the section's folders is not something to hide. */
|
|
21
|
+
notice?: ReactNode;
|
|
22
|
+
/** The section's folder rows. */
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One section of Mail's sidebar ("All mailboxes", or one mailbox): a heading over a list of folders. When `collapsible` the heading is a
|
|
28
|
+
* disclosure button (`aria-expanded`, `aria-controls` the list, a chevron that turns) and the list is `hidden` while it is collapsed -
|
|
29
|
+
* kept in the document, so the button's `aria-controls` always names something and nothing is fetched or rebuilt on opening it.
|
|
30
|
+
*/
|
|
31
|
+
export default function SidebarSection({ domId, label, collapsible, expanded, onToggle, unread = 0, notice, children }: SidebarSectionProps) {
|
|
32
|
+
const shown = !collapsible || expanded;
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
{collapsible ? (
|
|
36
|
+
<h2 className="mb-1">
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
aria-expanded={expanded}
|
|
40
|
+
aria-controls={domId}
|
|
41
|
+
onClick={onToggle}
|
|
42
|
+
className={[
|
|
43
|
+
"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",
|
|
44
|
+
"focus-visible:outline-2 focus-visible:outline-primary",
|
|
45
|
+
"hover:bg-surface-alt hover:text-text",
|
|
46
|
+
].join(" ")}
|
|
47
|
+
>
|
|
48
|
+
<HiChevronDown
|
|
49
|
+
size={14}
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
className={["shrink-0 transition-transform", expanded ? "" : "-rotate-90"].join(" ")}
|
|
52
|
+
/>
|
|
53
|
+
<span className="min-w-0 flex-1 truncate">{label}</span>
|
|
54
|
+
{!expanded && unread > 0 && <FolderBadgeChip badge={{ kind: "unread", value: unread }} />}
|
|
55
|
+
</button>
|
|
56
|
+
</h2>
|
|
57
|
+
) : (
|
|
58
|
+
<div className="text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate">{label}</div>
|
|
59
|
+
)}
|
|
60
|
+
{notice}
|
|
61
|
+
<div id={domId} hidden={!shown} className={shown ? "flex flex-col gap-0.5" : "hidden"}>
|
|
62
|
+
{children}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|