@rapidmx/web-client 0.15.1 → 0.16.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 +373 -373
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -480
- package/apps/admin/domains/[uid].tsx +164 -164
- package/apps/admin/escrow-scopes/[uid].tsx +348 -348
- package/apps/admin/index.tsx +127 -124
- package/apps/admin/mailboxes/[uid].tsx +269 -224
- package/apps/admin/mailboxes/new/index.tsx +28 -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/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/components/admin/layout/AdminShell.tsx +378 -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 +444 -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 +217 -216
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +480 -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 +612 -491
- package/apps/shared/components/mail/MailSelectionBar.tsx +236 -227
- package/apps/shared/components/mail/MessageDetailPane.tsx +1559 -1559
- 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 +137 -113
- package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
- package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -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 +255 -254
- package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
- package/apps/shared/components/tasks/layout/TasksShell.tsx +218 -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/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/primaryMailbox.ts +71 -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/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +178 -178
- package/apps/shared/navigation/AppRouter.tsx +300 -300
- package/apps/shared/navigation/routerContext.tsx +83 -83
- 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/calendar/index.tsx +578 -571
- package/apps/www/contacts/[uid].tsx +109 -109
- package/apps/www/contacts/index.tsx +6 -0
- package/apps/www/index.tsx +2847 -2479
- package/apps/www/messages/[uid].tsx +135 -135
- package/apps/www/settings/auto-reply/index.tsx +133 -133
- package/apps/www/settings/encryption/index.tsx +1287 -1287
- package/apps/www/settings/filters/[uid].tsx +176 -176
- package/apps/www/settings/filters/index.tsx +102 -102
- package/apps/www/settings/filters/new/index.tsx +140 -140
- package/apps/www/settings/labels/index.tsx +204 -204
- package/apps/www/settings/privacy/index.tsx +492 -492
- package/apps/www/settings/profile/index.tsx +248 -248
- package/apps/www/settings/read-receipts/index.tsx +147 -147
- package/apps/www/settings/sharing/index.tsx +256 -256
- package/apps/www/settings/signatures/[uid].tsx +172 -172
- package/apps/www/settings/signatures/index.tsx +88 -88
- package/apps/www/settings/signatures/new/index.tsx +135 -135
- package/apps/www/tasks/index.tsx +649 -649
- package/dist/apps/admin/data-requests/index.js +1 -1
- package/dist/apps/admin/index.js +2 -1
- package/dist/apps/admin/mailboxes/[uid].js +12 -2
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -2
- 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/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 +3 -2
- 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 +4 -3
- 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/MailSelectionBar.d.ts +6 -1
- package/dist/apps/shared/components/mail/MailSelectionBar.js +3 -3
- 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 +5 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +64 -45
- 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/PendingMessageCard.d.ts +14 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +6 -5
- package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +4 -3
- package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
- package/dist/apps/shared/mail/folderCounts.js +12 -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/primaryMailbox.d.ts +29 -0
- package/dist/apps/shared/mail/primaryMailbox.js +61 -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/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/calendar/index.js +3 -2
- package/dist/apps/www/contacts/index.js +3 -1
- package/dist/apps/www/index.js +426 -189
- package/package.json +2 -2
|
@@ -9,6 +9,7 @@ import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
|
9
9
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
10
10
|
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
11
11
|
import AppShell from "../../layout/AppShell.js";
|
|
12
|
+
import FloatingActionButton from "../../layout/FloatingActionButton.js";
|
|
12
13
|
import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
|
|
13
14
|
import KeyEnrollmentGate from "../../layout/KeyEnrollmentGate.js";
|
|
14
15
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
@@ -17,7 +18,11 @@ import LocalIndexLifecycle from "../../../search/LocalIndexLifecycle.js";
|
|
|
17
18
|
import { NO_LIVE_UPDATES } from "../../../mail/useMailLiveUpdates.js";
|
|
18
19
|
import { MAILBOX_LIST_LIMIT, MailConnectionContext, useMailConnection } from "../../../mail/useMailConnection.js";
|
|
19
20
|
import { folderRows } from "../../../mail/folderTree.js";
|
|
20
|
-
import {
|
|
21
|
+
import { primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
22
|
+
import { badgeFor, countOfFolder, inboxUnreadTotal, unreadBadgeTotal, } from "../../../mail/folderCounts.js";
|
|
23
|
+
import { ALL_MAILBOXES_SECTION, useSidebarSections } from "../../../mail/useCollapsedSections.js";
|
|
24
|
+
import FolderBadgeChip from "./FolderBadgeChip.js";
|
|
25
|
+
import SidebarSection from "./SidebarSection.js";
|
|
21
26
|
import { useUnreadTitle } from "../../../mail/useUnreadTitle.js";
|
|
22
27
|
import { pendingCountFor, usePendingSends } from "../../../mail/outbox/pendingSends.js";
|
|
23
28
|
import OutboxBadge from "../OutboxBadge.js";
|
|
@@ -66,13 +71,6 @@ function aggregateBadge(mailboxFolders, type, counts) {
|
|
|
66
71
|
}, { unread: 0, total: 0 });
|
|
67
72
|
return badgeFor(type, sum);
|
|
68
73
|
}
|
|
69
|
-
/** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
|
|
70
|
-
function FolderBadgeChip({ badge }) {
|
|
71
|
-
return (_jsxs("span", { className: [
|
|
72
|
-
"text-xs rounded-pill py-0.5 px-1.5",
|
|
73
|
-
badge.kind === "unread" ? "font-bold bg-primary/15 text-primary-dark" : "font-medium text-text-muted",
|
|
74
|
-
].join(" "), children: [_jsx("span", { "aria-hidden": "true", children: badge.value }), _jsxs("span", { className: "sr-only", children: [" ", badgeLabel(badge)] })] }));
|
|
75
|
-
}
|
|
76
74
|
export { MAILBOX_LIST_LIMIT };
|
|
77
75
|
/**
|
|
78
76
|
* A separate component (not inlined into `MailShell`'s own render) so `useCompose()` resolves against
|
|
@@ -98,7 +96,7 @@ function ComposeButton({ mailboxUid }) {
|
|
|
98
96
|
*/
|
|
99
97
|
function MobileComposeButton({ mailboxUid }) {
|
|
100
98
|
const { openCompose } = useCompose();
|
|
101
|
-
return (_jsx(
|
|
99
|
+
return (_jsx(FloatingActionButton, { label: "New message", icon: HiOutlinePencilSquare, onClick: () => openCompose({ mailboxUid }), onPointerEnter: prefetchComposeWindow, onFocus: prefetchComposeWindow }));
|
|
102
100
|
}
|
|
103
101
|
/**
|
|
104
102
|
* Mail's keyboard shortcuts that don't belong to a list or a message: "New message". A component of its own for the reason `ComposeButton`
|
|
@@ -125,6 +123,11 @@ function MailShortcuts({ mailboxUid }) {
|
|
|
125
123
|
* behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
|
|
126
124
|
* aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
|
|
127
125
|
* of folders" requirement.
|
|
126
|
+
*
|
|
127
|
+
* An address that names nothing opens "All Mailboxes > Inbox" when there is more than one mailbox (that is when the section exists), and
|
|
128
|
+
* the one mailbox's own Inbox otherwise; a `?mailboxUid=`, `?folderUid=` or `?aggregate=` always wins. Nothing is selected until the address
|
|
129
|
+
* has been read. What needs a single mailbox while an all-mailboxes view is open (Compose, the new-message shortcut, key unlocking) uses the
|
|
130
|
+
* primary mailbox.
|
|
128
131
|
*/
|
|
129
132
|
export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }) {
|
|
130
133
|
// The mailboxes, their folders and the live connection come from the persistent app frame when there is one (`AppChrome` owns them, so
|
|
@@ -140,6 +143,8 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
140
143
|
const [requestedFolderUid, setRequestedFolderUid] = useState(null);
|
|
141
144
|
const [requestedAggregateType, setRequestedAggregateType] = useState(null);
|
|
142
145
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
146
|
+
// Whether the URL has been read yet (see below): until then the selection is not known, and the sidebar sections have nothing to open for.
|
|
147
|
+
const [selectionRead, setSelectionRead] = useState(false);
|
|
143
148
|
// The selection lives in the URL, and the router (`AppRouter`) changes the URL without a page load when a folder link is
|
|
144
149
|
// clicked - so it is read from the router's location, which updates, rather than once from `window.location`. Still read
|
|
145
150
|
// in an effect, never during the first render, so the server render and the hydrating render agree.
|
|
@@ -149,31 +154,44 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
149
154
|
setRequestedMailboxUid(params.get("mailboxUid"));
|
|
150
155
|
setRequestedFolderUid(params.get("folderUid"));
|
|
151
156
|
setRequestedAggregateType(params.get("aggregate"));
|
|
157
|
+
setSelectionRead(true);
|
|
152
158
|
}, [search]);
|
|
153
159
|
// Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
|
|
154
160
|
// with the page - is closed here.
|
|
155
161
|
useEffect(() => {
|
|
156
162
|
setDrawerOpen(false);
|
|
157
163
|
}, [search]);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
// What the address asks for. Nothing is selected until it has been read (the first render, before the effect above): choosing a
|
|
165
|
+
// default then would list a folder - the most expensive being every mailbox's - only to throw it away for the one the address names.
|
|
166
|
+
const requestedAggregate = selectionRead && isAggregateFolderType(requestedAggregateType) ? requestedAggregateType : undefined;
|
|
167
|
+
const requestedMailbox = selectionRead && requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined;
|
|
168
|
+
// The mailbox the caller has not chosen: their own, never a shared one that merely sorts first. It is what compose, the new-message
|
|
169
|
+
// shortcut and everything else that needs a single mailbox use while an all-mailboxes view (which has none) is open.
|
|
170
|
+
const defaultMailboxUid = primaryMailboxUid(mailboxes, userUid);
|
|
171
|
+
// With several mailboxes the address that names nothing opens "All Mailboxes > Inbox", the merged Inbox; with one there is no such
|
|
172
|
+
// section, so it opens that mailbox's own Inbox. A mailbox, a folder or an all-mailboxes view in the address wins over either.
|
|
173
|
+
const aggregateByDefault = selectionRead && !requestedAggregate && !requestedMailbox && requestedFolderUid === null && mailboxes.length > 1;
|
|
174
|
+
const aggregateFolderType = requestedAggregate ?? (aggregateByDefault ? "inbox" : undefined);
|
|
175
|
+
const mailboxUid = aggregateFolderType || !selectionRead ? undefined : (requestedMailbox ?? defaultMailboxUid);
|
|
165
176
|
const selectedMailboxFolders = mailboxFolders.find((mf) => mf.mailbox.uid === mailboxUid)?.folders ?? [];
|
|
166
177
|
const folderUid = aggregateFolderType
|
|
167
178
|
? undefined
|
|
168
179
|
: (requestedFolderUid && selectedMailboxFolders.some((f) => f.uid === requestedFolderUid) ? requestedFolderUid : undefined) ??
|
|
169
180
|
selectedMailboxFolders.find((f) => f.type === "inbox")?.uid;
|
|
170
181
|
// The mailbox `KeyEnrollmentGate`/`LocalIndexLifecycle`/`ComposeButton` treat as "the" mailbox when
|
|
171
|
-
// there's no single selected one to use (aggregate mode) - the caller's own
|
|
172
|
-
// have one, else whichever accessible mailbox happens to be first. See MailShell's own doc comment on
|
|
182
|
+
// there's no single selected one to use (aggregate mode) - the caller's primary mailbox (`primaryMailbox()`). See MailShell's own doc comment on
|
|
173
183
|
// this being an accepted limitation: an aggregate-view message from a *different*, not-yet-visited
|
|
174
184
|
// mailbox may still need that mailbox's own folder view opened directly to unlock/decrypt it.
|
|
175
|
-
const defaultMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
|
|
176
185
|
const activeMailboxUid = mailboxUid ?? defaultMailboxUid;
|
|
186
|
+
// Which sidebar sections are open: "All mailboxes" and the primary mailbox unless the reader chose otherwise. Any of them can be collapsed;
|
|
187
|
+
// a change of the open folder into another section opens that one (see `useSidebarSections()`); the first selection, once the address has been read
|
|
188
|
+
// 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.
|
|
189
|
+
const sections = useSidebarSections({
|
|
190
|
+
userUid,
|
|
191
|
+
primaryUid: defaultMailboxUid,
|
|
192
|
+
activeId: aggregateFolderType ? ALL_MAILBOXES_SECTION : mailboxUid,
|
|
193
|
+
});
|
|
194
|
+
const collapsible = mailboxes.length > 1;
|
|
177
195
|
const counts = folderCounts.counts;
|
|
178
196
|
const trackMessageChange = folderCounts.track;
|
|
179
197
|
// `(3) Acme: Mail` in the tab strip while there is unread mail in an Inbox - kept by the frame when there is one.
|
|
@@ -201,32 +219,33 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
|
|
|
201
219
|
// A function, not a plain JSX constant — it's rendered twice (desktop `<aside>` + mobile
|
|
202
220
|
// `Drawer`), possibly *simultaneously* mounted (the aside is only CSS-hidden below `md`, not
|
|
203
221
|
// unmounted).
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
222
|
+
// `idPrefix` keeps each section's folder list's id (which its heading's `aria-controls` names) unique between the two.
|
|
223
|
+
const sidebarContent = (idPrefix) => (_jsxs(_Fragment, { children: [_jsx("div", { className: "p-3", children: _jsx(ComposeButton, { mailboxUid: defaultMailboxUid }) }), foldersLoading ? (_jsx("div", { className: "flex-1 overflow-y-auto px-3 pb-3", children: _jsx(SkeletonList, { count: 6, className: "pt-1" }) })) : (_jsxs("nav", { className: "flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3", children: [mailboxes.length > 1 && (_jsx(SidebarSection, { domId: `${idPrefix}-mail-section-${ALL_MAILBOXES_SECTION}`, label: "All Mailboxes", collapsible: collapsible, expanded: sections.isExpanded(ALL_MAILBOXES_SECTION), onToggle: () => sections.toggle(ALL_MAILBOXES_SECTION), unread: unreadBadgeTotal(mailboxFolders.flatMap((mf) => mf.folders), counts), children: AGGREGATE_FOLDER_TYPES.map((type) => {
|
|
224
|
+
const badge = aggregateBadge(mailboxFolders, type, counts);
|
|
225
|
+
return (_jsxs("a", { href: `/?aggregate=${encodeURIComponent(type)}`, className: [
|
|
226
|
+
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
227
|
+
aggregateFolderType === type
|
|
228
|
+
? "bg-primary/10 text-primary-dark font-semibold"
|
|
229
|
+
: "text-text hover:bg-surface-alt",
|
|
230
|
+
].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[type] }), badge && _jsx(FolderBadgeChip, { badge: badge })] }, type));
|
|
231
|
+
}) })), mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (_jsx(SidebarSection, { domId: `${idPrefix}-mail-section-${mailbox.uid}`, label: _jsxs(_Fragment, { children: [mailbox.displayName, mailbox.ownerUserUid ? "" : " (shared)"] }), collapsible: collapsible, expanded: sections.isExpanded(mailbox.uid), onToggle: () => sections.toggle(mailbox.uid), unread: unreadBadgeTotal(folders, counts), notice: mailboxError && (_jsx("div", { className: "px-2.5 pb-1", children: _jsx(Alert, { children: mailboxError }) })), children: folderRows(folders, pendingCountFor(mailbox.uid, pendingSends)).map((row) => {
|
|
232
|
+
if (row.kind === "placeholder") {
|
|
233
|
+
// A folder the server has not made (or told us about) yet - the Outbox and Sent Items are made with the first message
|
|
234
|
+
// sent - shown the moment a message is on its way. Nothing to open; the real folder replaces it when it arrives.
|
|
235
|
+
const sending = pendingCountFor(mailbox.uid, pendingSends);
|
|
236
|
+
return (_jsxs("div", { "data-folder-placeholder": row.type, className: "flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5 text-text-muted", children: [_jsx("span", { children: FOLDER_LABELS[row.type] }), row.type === "outbox" && _jsx(OutboxBadge, { total: sending, pendingHere: sending })] }, `placeholder-${row.type}`));
|
|
237
|
+
}
|
|
238
|
+
const folder = row.folder;
|
|
239
|
+
const badge = badgeFor(folder.type, countOfFolder(folder, counts));
|
|
240
|
+
return (_jsxs("a", { href: `/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`, className: [
|
|
241
|
+
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
242
|
+
folder.uid === folderUid
|
|
243
|
+
? "bg-primary/10 text-primary-dark font-semibold"
|
|
244
|
+
: "text-text hover:bg-surface-alt",
|
|
245
|
+
].join(" "), children: [_jsx("span", { className: badge?.kind === "unread" ? "font-semibold" : undefined, children: FOLDER_LABELS[folder.type] ?? folder.name }), badge &&
|
|
246
|
+
(folder.type === "outbox" ? (_jsx(OutboxBadge, { total: badge.value, status: outboxStatuses[folder.uid], pendingHere: pendingCountFor(mailbox.uid, pendingSends) })) : (_jsx(FolderBadgeChip, { badge: badge })))] }, folder.uid));
|
|
247
|
+
}) }, mailbox.uid)))] }))] }));
|
|
248
|
+
inner = (_jsxs(_Fragment, { children: [_jsx(MailShortcuts, { mailboxUid: defaultMailboxUid }), _jsx("aside", { className: "hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Folders", fullScreen: true, children: _jsx("div", { className: "flex flex-col", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 overflow-y-auto", children: [_jsxs("div", { className: "md:hidden flex items-center gap-2 p-3", children: [_jsx("button", { type: "button", className: "shrink-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open folders", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("div", { ref: setMobileSearchSlot, className: "flex-1 min-w-0" })] }), _jsx(MobileComposeButton, { mailboxUid: defaultMailboxUid }), _jsx(MailShellContext.Provider, { value: contextValue, children: children })] })] }));
|
|
230
249
|
}
|
|
231
250
|
// Wraps AppShell unconditionally, at a stable tree position regardless of whether mailboxUid has
|
|
232
251
|
// resolved yet - KeyEnrollmentGate itself passes `children` through untouched until a real
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
export interface SidebarSectionProps {
|
|
3
|
+
/** Unique per rendering of the sidebar (it is on screen twice on a phone: beside the list and in the drawer): names the folder list the heading controls. */
|
|
4
|
+
domId: string;
|
|
5
|
+
/** The heading's text. */
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
/** Whether the heading is a toggle at all. Not for a lone mailbox, whose section has nothing to be hidden for. */
|
|
8
|
+
collapsible: boolean;
|
|
9
|
+
expanded: boolean;
|
|
10
|
+
onToggle: () => void;
|
|
11
|
+
/** The unread mail in the section's folders that carry an unread count (see `unreadBadgeTotal()`), shown on the heading while it is collapsed (and only when there is some) so new mail in a hidden section is still noticed. */
|
|
12
|
+
unread?: number;
|
|
13
|
+
/** Shown between the heading and the folders, collapsed or not: a failure to load the section's folders is not something to hide. */
|
|
14
|
+
notice?: ReactNode;
|
|
15
|
+
/** The section's folder rows. */
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* One section of Mail's sidebar ("All mailboxes", or one mailbox): a heading over a list of folders. When `collapsible` the heading is a
|
|
20
|
+
* disclosure button (`aria-expanded`, `aria-controls` the list, a chevron that turns) and the list is `hidden` while it is collapsed -
|
|
21
|
+
* kept in the document, so the button's `aria-controls` always names something and nothing is fetched or rebuilt on opening it.
|
|
22
|
+
*/
|
|
23
|
+
export default function SidebarSection({ domId, label, collapsible, expanded, onToggle, unread, notice, children }: SidebarSectionProps): React.JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HiChevronDown } from "react-icons/hi2";
|
|
3
|
+
import FolderBadgeChip from "./FolderBadgeChip.js";
|
|
4
|
+
/**
|
|
5
|
+
* One section of Mail's sidebar ("All mailboxes", or one mailbox): a heading over a list of folders. When `collapsible` the heading is a
|
|
6
|
+
* disclosure button (`aria-expanded`, `aria-controls` the list, a chevron that turns) and the list is `hidden` while it is collapsed -
|
|
7
|
+
* kept in the document, so the button's `aria-controls` always names something and nothing is fetched or rebuilt on opening it.
|
|
8
|
+
*/
|
|
9
|
+
export default function SidebarSection({ domId, label, collapsible, expanded, onToggle, unread = 0, notice, children }) {
|
|
10
|
+
const shown = !collapsible || expanded;
|
|
11
|
+
return (_jsxs("div", { children: [collapsible ? (_jsx("h2", { className: "mb-1", children: _jsxs("button", { type: "button", "aria-expanded": expanded, "aria-controls": domId, onClick: onToggle, className: [
|
|
12
|
+
"flex w-full items-center gap-1 rounded-sm px-2.5 py-0.5 text-left text-xs font-bold uppercase tracking-wide text-text-muted",
|
|
13
|
+
"focus-visible:outline-2 focus-visible:outline-primary",
|
|
14
|
+
"hover:bg-surface-alt hover:text-text",
|
|
15
|
+
].join(" "), children: [_jsx(HiChevronDown, { size: 14, "aria-hidden": "true", className: ["shrink-0 transition-transform", expanded ? "" : "-rotate-90"].join(" ") }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: label }), !expanded && unread > 0 && _jsx(FolderBadgeChip, { badge: { kind: "unread", value: unread } })] }) })) : (_jsx("div", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-1 px-2.5 truncate", children: label })), notice, _jsx("div", { id: domId, hidden: !shown, className: shown ? "flex flex-col gap-0.5" : "hidden", children: children })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { OutgoingReply } from "../../../mail/outbox/outgoingReplies.js";
|
|
3
|
+
/**
|
|
4
|
+
* A reply or forward that has just been sent, drawn where its message will be once the server has filed it: the sender, who it went to, its body as
|
|
5
|
+
* it was composed, and one line saying where it is - "Sending...", "Sent", or, when it did not go, "Not sent: <why>" with the same ways out the
|
|
6
|
+
* pop-up offers (Retry, Open draft ...). It reads like the message's own card, so the real message replaces it without the thread rearranging.
|
|
7
|
+
*
|
|
8
|
+
* The body goes through the reading pane's own sanitizing frame (`MessageBody`), exactly as a received message's does.
|
|
9
|
+
*/
|
|
10
|
+
export default function PendingMessageCard({ reply, headerRef, }: {
|
|
11
|
+
reply: OutgoingReply;
|
|
12
|
+
/** Registered so the pane can put the focus here when the message first appears. Focusable, but not in the tab order. */
|
|
13
|
+
headerRef: (node: HTMLHeadingElement | null) => void;
|
|
14
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
3
|
+
import MailAddress, { RecipientLine } from "../MailAddress.js";
|
|
4
|
+
import MessageBody from "./MessageBody.js";
|
|
5
|
+
import { BODY_FONT_STYLE, CardShell, SenderAvatar } from "./MessageCard.js";
|
|
6
|
+
/**
|
|
7
|
+
* A reply or forward that has just been sent, drawn where its message will be once the server has filed it: the sender, who it went to, its body as
|
|
8
|
+
* it was composed, and one line saying where it is - "Sending...", "Sent", or, when it did not go, "Not sent: <why>" with the same ways out the
|
|
9
|
+
* pop-up offers (Retry, Open draft ...). It reads like the message's own card, so the real message replaces it without the thread rearranging.
|
|
10
|
+
*
|
|
11
|
+
* The body goes through the reading pane's own sanitizing frame (`MessageBody`), exactly as a received message's does.
|
|
12
|
+
*/
|
|
13
|
+
export default function PendingMessageCard({ reply, headerRef, }) {
|
|
14
|
+
const failed = reply.state === "failed";
|
|
15
|
+
return (_jsxs(CardShell, { children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [_jsx(SenderAvatar, { from: reply.sender }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-x-2 text-sm", children: [_jsx("h2", { ref: headerRef, tabIndex: -1, style: BODY_FONT_STYLE, className: "flex-1 min-w-[13rem] font-semibold outline-none focus-visible:outline-2 focus-visible:outline-primary", children: _jsx(MailAddress, { recipient: reply.sender }) }), _jsxs("div", { role: failed ? "alert" : "status", "data-testid": "pending-message-status", "data-state": reply.state, className: ["flex items-center gap-1.5 text-xs shrink-0", failed ? "font-semibold text-danger" : "text-text-muted"].join(" "), children: [reply.state === "sending" && _jsx("span", { "aria-hidden": "true", className: "rr-sending-dot h-1.5 w-1.5 shrink-0 rounded-full bg-primary" }), _jsx("span", { children: failed ? `Not sent: ${reply.failure}` : reply.state === "sending" ? "Sending…" : "Sent" })] }, failed ? "failed" : "progress")] }), _jsx(RecipientLine, { label: "To", recipients: reply.to }), _jsx(RecipientLine, { label: "Cc", recipients: reply.cc }), _jsx(RecipientLine, { label: "Bcc", recipients: reply.bcc })] })] }), failed && (_jsx("div", { className: "flex flex-wrap gap-2 px-4 pb-3", children: reply.actions.map((action) => (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: action.onClick, children: action.label }, action.label))) })), _jsx("div", { className: "px-4 pb-4", children: _jsx(MessageBody, { messageUid: reply.uid, messageVersion: 0, title: `Message: ${reply.subject}`, content: { kind: "html", html: reply.html } }) })] }));
|
|
16
|
+
}
|
|
@@ -13,6 +13,7 @@ import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedbac
|
|
|
13
13
|
import AppShell from "../../layout/AppShell.js";
|
|
14
14
|
import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
|
|
15
15
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
16
|
+
import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
16
17
|
import { mergePluginNavItems } from "../../../plugins/pluginNav.js";
|
|
17
18
|
/** Every settings section with its own sidebar entry. */
|
|
18
19
|
export const SETTINGS_SECTIONS = [
|
|
@@ -73,7 +74,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
73
74
|
}
|
|
74
75
|
listMailboxes({ limit: 100 })
|
|
75
76
|
.then((result) => {
|
|
76
|
-
setMailboxes(result);
|
|
77
|
+
setMailboxes(orderMailboxes(result, userUid));
|
|
77
78
|
setStatus("ready");
|
|
78
79
|
})
|
|
79
80
|
.catch((err) => {
|
|
@@ -82,7 +83,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
82
83
|
});
|
|
83
84
|
}, [userUid]);
|
|
84
85
|
const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
|
|
85
|
-
mailboxes
|
|
86
|
+
primaryMailboxUid(mailboxes, userUid);
|
|
86
87
|
const contextValue = useMemo(() => ({ mailboxUid, mailboxes }), [mailboxUid, mailboxes]);
|
|
87
88
|
// A full-screen takeover, not nested inside the rest of the app's chrome — same as ContactsShell/
|
|
88
89
|
// TasksShell, there's nothing else for a mailbox-less caller to configure here yet.
|
|
@@ -110,8 +111,8 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
110
111
|
}, 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
112
|
// `inner` (this whole branch) is only ever computed once `status === "ready"`
|
|
112
113
|
// and `mailboxUid` has already resolved truthy — a falsy `mailboxUid` returns
|
|
113
|
-
// `<MailboxProvisioning />` above instead (
|
|
114
|
-
//
|
|
114
|
+
// `<MailboxProvisioning />` above instead (which is what the `!` below records,
|
|
115
|
+
// `primaryMailboxUid()` being honest that it may find none) — so no fallback is
|
|
115
116
|
// needed here, matching this plan's established "dead guard the UI
|
|
116
117
|
// structurally can't trigger" removal precedent (e.g.
|
|
117
118
|
// `DomainDetailContent.handleVerify`).
|
|
@@ -119,7 +120,7 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
|
|
|
119
120
|
"block px-2.5 py-1.5 rounded-sm text-sm",
|
|
120
121
|
section.id === active ? "bg-primary/10 text-primary-dark font-medium" : "text-text hover:bg-surface-alt",
|
|
121
122
|
].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 })] })] }));
|
|
123
|
+
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
124
|
}
|
|
124
125
|
return (_jsx(AppShell, { active: "settings", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
|
|
125
126
|
}
|
|
@@ -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
|
}
|
|
@@ -13,6 +13,7 @@ import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedbac
|
|
|
13
13
|
import AppShell from "../../layout/AppShell.js";
|
|
14
14
|
import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
|
|
15
15
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
16
|
+
import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
16
17
|
const TasksShellContext = createContext({ mailboxes: [] });
|
|
17
18
|
/** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
|
|
18
19
|
export function useTasksShell() {
|
|
@@ -51,7 +52,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
51
52
|
}
|
|
52
53
|
listMailboxes({ limit: 100 })
|
|
53
54
|
.then((result) => {
|
|
54
|
-
setMailboxes(result);
|
|
55
|
+
setMailboxes(orderMailboxes(result, userUid));
|
|
55
56
|
setStatus("ready");
|
|
56
57
|
})
|
|
57
58
|
.catch((err) => {
|
|
@@ -60,7 +61,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
60
61
|
});
|
|
61
62
|
}, [userUid]);
|
|
62
63
|
const mailboxUid = (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
|
|
63
|
-
mailboxes
|
|
64
|
+
primaryMailboxUid(mailboxes, userUid);
|
|
64
65
|
useEffect(() => {
|
|
65
66
|
if (!mailboxUid) {
|
|
66
67
|
setFolders([]);
|
|
@@ -98,7 +99,7 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
|
|
|
98
99
|
}, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), folderError && _jsx(Alert, { children: folderError })] }));
|
|
99
100
|
// With a single mailbox and no error this column would be empty: no 224 px of nothing beside the tasks' own menu.
|
|
100
101
|
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 })] })] }));
|
|
102
|
+
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
103
|
}
|
|
103
104
|
return (_jsx(AppShell, { active: "tasks", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
|
|
104
105
|
}
|
|
@@ -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,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;
|