@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 { 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 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,15 +18,19 @@ import LocalIndexLifecycle from "../../../search/LocalIndexLifecycle.js";
|
|
|
17
18
|
import { LiveUpdates, 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";
|
|
21
|
+
import { primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
20
22
|
import {
|
|
21
23
|
CountTracker,
|
|
22
24
|
FolderBadge,
|
|
23
25
|
FolderCount,
|
|
24
26
|
badgeFor,
|
|
25
|
-
badgeLabel,
|
|
26
27
|
countOfFolder,
|
|
27
28
|
inboxUnreadTotal,
|
|
29
|
+
unreadBadgeTotal,
|
|
28
30
|
} from "../../../mail/folderCounts.js";
|
|
31
|
+
import { ALL_MAILBOXES_SECTION, useSidebarSections } from "../../../mail/useCollapsedSections.js";
|
|
32
|
+
import FolderBadgeChip from "./FolderBadgeChip.js";
|
|
33
|
+
import SidebarSection from "./SidebarSection.js";
|
|
29
34
|
import { useUnreadTitle } from "../../../mail/useUnreadTitle.js";
|
|
30
35
|
import { pendingCountFor, usePendingSends } from "../../../mail/outbox/pendingSends.js";
|
|
31
36
|
import OutboxBadge from "../OutboxBadge.js";
|
|
@@ -143,21 +148,6 @@ function aggregateBadge(mailboxFolders: MailboxFolders[], type: AggregateFolderT
|
|
|
143
148
|
return badgeFor(type, sum);
|
|
144
149
|
}
|
|
145
150
|
|
|
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
151
|
type Status = "checking" | "error" | "ready";
|
|
162
152
|
|
|
163
153
|
export { MAILBOX_LIST_LIMIT };
|
|
@@ -200,16 +190,13 @@ function ComposeButton({ mailboxUid }: { mailboxUid?: string }) {
|
|
|
200
190
|
function MobileComposeButton({ mailboxUid }: { mailboxUid?: string }) {
|
|
201
191
|
const { openCompose } = useCompose();
|
|
202
192
|
return (
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
193
|
+
<FloatingActionButton
|
|
194
|
+
label="New message"
|
|
195
|
+
icon={HiOutlinePencilSquare}
|
|
206
196
|
onClick={() => openCompose({ mailboxUid })}
|
|
207
197
|
onPointerEnter={prefetchComposeWindow}
|
|
208
198
|
onFocus={prefetchComposeWindow}
|
|
209
|
-
|
|
210
|
-
>
|
|
211
|
-
<HiOutlinePencilSquare size={22} aria-hidden="true" />
|
|
212
|
-
</button>
|
|
199
|
+
/>
|
|
213
200
|
);
|
|
214
201
|
}
|
|
215
202
|
|
|
@@ -239,6 +226,11 @@ function MailShortcuts({ mailboxUid }: { mailboxUid?: string }) {
|
|
|
239
226
|
* behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
|
|
240
227
|
* aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
|
|
241
228
|
* of folders" requirement.
|
|
229
|
+
*
|
|
230
|
+
* An address that names nothing opens "All Mailboxes > Inbox" when there is more than one mailbox (that is when the section exists), and
|
|
231
|
+
* the one mailbox's own Inbox otherwise; a `?mailboxUid=`, `?folderUid=` or `?aggregate=` always wins. Nothing is selected until the address
|
|
232
|
+
* has been read. What needs a single mailbox while an all-mailboxes view is open (Compose, the new-message shortcut, key unlocking) uses the
|
|
233
|
+
* primary mailbox.
|
|
242
234
|
*/
|
|
243
235
|
export default function MailShell({
|
|
244
236
|
userUid,
|
|
@@ -263,6 +255,8 @@ export default function MailShell({
|
|
|
263
255
|
const [requestedFolderUid, setRequestedFolderUid] = useState<string | null>(null);
|
|
264
256
|
const [requestedAggregateType, setRequestedAggregateType] = useState<string | null>(null);
|
|
265
257
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
258
|
+
// 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.
|
|
259
|
+
const [selectionRead, setSelectionRead] = useState(false);
|
|
266
260
|
|
|
267
261
|
// The selection lives in the URL, and the router (`AppRouter`) changes the URL without a page load when a folder link is
|
|
268
262
|
// clicked - so it is read from the router's location, which updates, rather than once from `window.location`. Still read
|
|
@@ -273,6 +267,7 @@ export default function MailShell({
|
|
|
273
267
|
setRequestedMailboxUid(params.get("mailboxUid"));
|
|
274
268
|
setRequestedFolderUid(params.get("folderUid"));
|
|
275
269
|
setRequestedAggregateType(params.get("aggregate"));
|
|
270
|
+
setSelectionRead(true);
|
|
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,23 @@ export default function MailShell({
|
|
|
280
275
|
setDrawerOpen(false);
|
|
281
276
|
}, [search]);
|
|
282
277
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
278
|
+
// What the address asks for. Nothing is selected until it has been read (the first render, before the effect above): choosing a
|
|
279
|
+
// default then would list a folder - the most expensive being every mailbox's - only to throw it away for the one the address names.
|
|
280
|
+
const requestedAggregate: AggregateFolderType | undefined =
|
|
281
|
+
selectionRead && isAggregateFolderType(requestedAggregateType) ? requestedAggregateType : undefined;
|
|
282
|
+
const requestedMailbox: string | undefined =
|
|
283
|
+
selectionRead && requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined;
|
|
286
284
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
285
|
+
// The mailbox the caller has not chosen: their own, never a shared one that merely sorts first. It is what compose, the new-message
|
|
286
|
+
// shortcut and everything else that needs a single mailbox use while an all-mailboxes view (which has none) is open.
|
|
287
|
+
const defaultMailboxUid = primaryMailboxUid(mailboxes, userUid);
|
|
288
|
+
|
|
289
|
+
// With several mailboxes the address that names nothing opens "All Mailboxes > Inbox", the merged Inbox; with one there is no such
|
|
290
|
+
// section, so it opens that mailbox's own Inbox. A mailbox, a folder or an all-mailboxes view in the address wins over either.
|
|
291
|
+
const aggregateByDefault =
|
|
292
|
+
selectionRead && !requestedAggregate && !requestedMailbox && requestedFolderUid === null && mailboxes.length > 1;
|
|
293
|
+
const aggregateFolderType: AggregateFolderType | undefined = requestedAggregate ?? (aggregateByDefault ? "inbox" : undefined);
|
|
294
|
+
const mailboxUid: string | undefined = aggregateFolderType || !selectionRead ? undefined : (requestedMailbox ?? defaultMailboxUid);
|
|
291
295
|
|
|
292
296
|
const selectedMailboxFolders = mailboxFolders.find((mf) => mf.mailbox.uid === mailboxUid)?.folders ?? [];
|
|
293
297
|
const folderUid: string | undefined = aggregateFolderType
|
|
@@ -296,13 +300,21 @@ export default function MailShell({
|
|
|
296
300
|
selectedMailboxFolders.find((f) => f.type === "inbox")?.uid;
|
|
297
301
|
|
|
298
302
|
// 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
|
|
303
|
+
// there's no single selected one to use (aggregate mode) - the caller's primary mailbox (`primaryMailbox()`). See MailShell's own doc comment on
|
|
301
304
|
// this being an accepted limitation: an aggregate-view message from a *different*, not-yet-visited
|
|
302
305
|
// 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
306
|
const activeMailboxUid = mailboxUid ?? defaultMailboxUid;
|
|
305
307
|
|
|
308
|
+
// Which sidebar sections are open: "All mailboxes" and the primary mailbox unless the reader chose otherwise. Any of them can be collapsed;
|
|
309
|
+
// a change of the open folder into another section opens that one (see `useSidebarSections()`); the first selection, once the address has been read
|
|
310
|
+
// 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.
|
|
311
|
+
const sections = useSidebarSections({
|
|
312
|
+
userUid,
|
|
313
|
+
primaryUid: defaultMailboxUid,
|
|
314
|
+
activeId: aggregateFolderType ? ALL_MAILBOXES_SECTION : mailboxUid,
|
|
315
|
+
});
|
|
316
|
+
const collapsible = mailboxes.length > 1;
|
|
317
|
+
|
|
306
318
|
const counts = folderCounts.counts;
|
|
307
319
|
const trackMessageChange = folderCounts.track;
|
|
308
320
|
// `(3) Acme: Mail` in the tab strip while there is unread mail in an Inbox - kept by the frame when there is one.
|
|
@@ -345,7 +357,8 @@ export default function MailShell({
|
|
|
345
357
|
// A function, not a plain JSX constant — it's rendered twice (desktop `<aside>` + mobile
|
|
346
358
|
// `Drawer`), possibly *simultaneously* mounted (the aside is only CSS-hidden below `md`, not
|
|
347
359
|
// unmounted).
|
|
348
|
-
|
|
360
|
+
// `idPrefix` keeps each section's folder list's id (which its heading's `aria-controls` names) unique between the two.
|
|
361
|
+
const sidebarContent = (idPrefix: string) => (
|
|
349
362
|
<>
|
|
350
363
|
<div className="p-3">
|
|
351
364
|
<ComposeButton mailboxUid={defaultMailboxUid} />
|
|
@@ -357,91 +370,102 @@ export default function MailShell({
|
|
|
357
370
|
) : (
|
|
358
371
|
<nav className="flex-1 overflow-y-auto px-3 pb-3 flex flex-col gap-3">
|
|
359
372
|
{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 (
|
|
373
|
+
<SidebarSection
|
|
374
|
+
domId={`${idPrefix}-mail-section-${ALL_MAILBOXES_SECTION}`}
|
|
375
|
+
label="All Mailboxes"
|
|
376
|
+
collapsible={collapsible}
|
|
377
|
+
expanded={sections.isExpanded(ALL_MAILBOXES_SECTION)}
|
|
378
|
+
onToggle={() => sections.toggle(ALL_MAILBOXES_SECTION)}
|
|
379
|
+
unread={unreadBadgeTotal(mailboxFolders.flatMap((mf) => mf.folders), counts)}
|
|
380
|
+
>
|
|
381
|
+
{AGGREGATE_FOLDER_TYPES.map((type) => {
|
|
382
|
+
const badge = aggregateBadge(mailboxFolders, type, counts);
|
|
383
|
+
return (
|
|
417
384
|
<a
|
|
418
|
-
key={
|
|
419
|
-
href={`/?
|
|
385
|
+
key={type}
|
|
386
|
+
href={`/?aggregate=${encodeURIComponent(type)}`}
|
|
420
387
|
className={[
|
|
421
388
|
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
422
|
-
|
|
389
|
+
aggregateFolderType === type
|
|
423
390
|
? "bg-primary/10 text-primary-dark font-semibold"
|
|
424
391
|
: "text-text hover:bg-surface-alt",
|
|
425
392
|
].join(" ")}
|
|
426
393
|
>
|
|
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
|
-
))}
|
|
394
|
+
<span className={badge?.kind === "unread" ? "font-semibold" : undefined}>{FOLDER_LABELS[type]}</span>
|
|
395
|
+
{badge && <FolderBadgeChip badge={badge} />}
|
|
440
396
|
</a>
|
|
397
|
+
);
|
|
398
|
+
})}
|
|
399
|
+
</SidebarSection>
|
|
400
|
+
)}
|
|
401
|
+
{mailboxFolders.map(({ mailbox, folders, error: mailboxError }) => (
|
|
402
|
+
<SidebarSection
|
|
403
|
+
key={mailbox.uid}
|
|
404
|
+
domId={`${idPrefix}-mail-section-${mailbox.uid}`}
|
|
405
|
+
label={
|
|
406
|
+
<>
|
|
407
|
+
{mailbox.displayName}
|
|
408
|
+
{mailbox.ownerUserUid ? "" : " (shared)"}
|
|
409
|
+
</>
|
|
410
|
+
}
|
|
411
|
+
collapsible={collapsible}
|
|
412
|
+
expanded={sections.isExpanded(mailbox.uid)}
|
|
413
|
+
onToggle={() => sections.toggle(mailbox.uid)}
|
|
414
|
+
unread={unreadBadgeTotal(folders, counts)}
|
|
415
|
+
notice={
|
|
416
|
+
mailboxError && (
|
|
417
|
+
<div className="px-2.5 pb-1">
|
|
418
|
+
<Alert>{mailboxError}</Alert>
|
|
419
|
+
</div>
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
>
|
|
423
|
+
{folderRows(folders, pendingCountFor(mailbox.uid, pendingSends)).map((row) => {
|
|
424
|
+
if (row.kind === "placeholder") {
|
|
425
|
+
// A folder the server has not made (or told us about) yet - the Outbox and Sent Items are made with the first message
|
|
426
|
+
// sent - shown the moment a message is on its way. Nothing to open; the real folder replaces it when it arrives.
|
|
427
|
+
const sending = pendingCountFor(mailbox.uid, pendingSends);
|
|
428
|
+
return (
|
|
429
|
+
<div
|
|
430
|
+
key={`placeholder-${row.type}`}
|
|
431
|
+
data-folder-placeholder={row.type}
|
|
432
|
+
className="flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5 text-text-muted"
|
|
433
|
+
>
|
|
434
|
+
<span>{FOLDER_LABELS[row.type]}</span>
|
|
435
|
+
{row.type === "outbox" && <OutboxBadge total={sending} pendingHere={sending} />}
|
|
436
|
+
</div>
|
|
441
437
|
);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
|
|
438
|
+
}
|
|
439
|
+
const folder = row.folder;
|
|
440
|
+
const badge = badgeFor(folder.type, countOfFolder(folder, counts));
|
|
441
|
+
return (
|
|
442
|
+
<a
|
|
443
|
+
key={folder.uid}
|
|
444
|
+
href={`/?mailboxUid=${encodeURIComponent(mailbox.uid)}&folderUid=${encodeURIComponent(folder.uid)}`}
|
|
445
|
+
className={[
|
|
446
|
+
"flex items-center justify-between text-sm rounded-sm py-1.5 px-2.5",
|
|
447
|
+
folder.uid === folderUid
|
|
448
|
+
? "bg-primary/10 text-primary-dark font-semibold"
|
|
449
|
+
: "text-text hover:bg-surface-alt",
|
|
450
|
+
].join(" ")}
|
|
451
|
+
>
|
|
452
|
+
<span className={badge?.kind === "unread" ? "font-semibold" : undefined}>
|
|
453
|
+
{FOLDER_LABELS[folder.type] ?? folder.name}
|
|
454
|
+
</span>
|
|
455
|
+
{badge &&
|
|
456
|
+
(folder.type === "outbox" ? (
|
|
457
|
+
<OutboxBadge
|
|
458
|
+
total={badge.value}
|
|
459
|
+
status={outboxStatuses[folder.uid]}
|
|
460
|
+
pendingHere={pendingCountFor(mailbox.uid, pendingSends)}
|
|
461
|
+
/>
|
|
462
|
+
) : (
|
|
463
|
+
<FolderBadgeChip badge={badge} />
|
|
464
|
+
))}
|
|
465
|
+
</a>
|
|
466
|
+
);
|
|
467
|
+
})}
|
|
468
|
+
</SidebarSection>
|
|
445
469
|
))}
|
|
446
470
|
</nav>
|
|
447
471
|
)}
|
|
@@ -451,9 +475,9 @@ export default function MailShell({
|
|
|
451
475
|
inner = (
|
|
452
476
|
<>
|
|
453
477
|
<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>
|
|
478
|
+
<aside className="hidden md:flex w-64 shrink-0 bg-surface border-r border-border flex-col">{sidebarContent("desktop")}</aside>
|
|
479
|
+
<Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Folders" fullScreen>
|
|
480
|
+
<div className="flex flex-col">{sidebarContent("mobile")}</div>
|
|
457
481
|
</Drawer>
|
|
458
482
|
<main className="flex-1 min-w-0 overflow-y-auto">
|
|
459
483
|
<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
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
7
|
+
import type { OutgoingReply } from "../../../mail/outbox/outgoingReplies.js";
|
|
8
|
+
import MailAddress, { RecipientLine } from "../MailAddress.js";
|
|
9
|
+
import MessageBody from "./MessageBody.js";
|
|
10
|
+
import { BODY_FONT_STYLE, CardShell, SenderAvatar } from "./MessageCard.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 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
|
|
14
|
+
* 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
|
|
15
|
+
* pop-up offers (Retry, Open draft ...). It reads like the message's own card, so the real message replaces it without the thread rearranging.
|
|
16
|
+
*
|
|
17
|
+
* The body goes through the reading pane's own sanitizing frame (`MessageBody`), exactly as a received message's does.
|
|
18
|
+
*/
|
|
19
|
+
export default function PendingMessageCard({
|
|
20
|
+
reply,
|
|
21
|
+
headerRef,
|
|
22
|
+
}: {
|
|
23
|
+
reply: OutgoingReply;
|
|
24
|
+
/** Registered so the pane can put the focus here when the message first appears. Focusable, but not in the tab order. */
|
|
25
|
+
headerRef: (node: HTMLHeadingElement | null) => void;
|
|
26
|
+
}) {
|
|
27
|
+
const failed = reply.state === "failed";
|
|
28
|
+
return (
|
|
29
|
+
<CardShell>
|
|
30
|
+
<div className="flex items-center gap-3 px-4 py-3">
|
|
31
|
+
<SenderAvatar from={reply.sender} />
|
|
32
|
+
<div className="flex-1 min-w-0">
|
|
33
|
+
<div className="flex flex-wrap items-baseline justify-between gap-x-2 text-sm">
|
|
34
|
+
<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">
|
|
35
|
+
<MailAddress recipient={reply.sender} />
|
|
36
|
+
</h2>
|
|
37
|
+
<div
|
|
38
|
+
// Its own element for each: an alert is announced when it appears, which a status that changed its role would not be.
|
|
39
|
+
key={failed ? "failed" : "progress"}
|
|
40
|
+
role={failed ? "alert" : "status"}
|
|
41
|
+
data-testid="pending-message-status"
|
|
42
|
+
data-state={reply.state}
|
|
43
|
+
className={["flex items-center gap-1.5 text-xs shrink-0", failed ? "font-semibold text-danger" : "text-text-muted"].join(" ")}
|
|
44
|
+
>
|
|
45
|
+
{reply.state === "sending" && <span aria-hidden="true" className="rr-sending-dot h-1.5 w-1.5 shrink-0 rounded-full bg-primary" />}
|
|
46
|
+
<span>{failed ? `Not sent: ${reply.failure}` : reply.state === "sending" ? "Sending…" : "Sent"}</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<RecipientLine label="To" recipients={reply.to} />
|
|
50
|
+
<RecipientLine label="Cc" recipients={reply.cc} />
|
|
51
|
+
<RecipientLine label="Bcc" recipients={reply.bcc} />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
{failed && (
|
|
55
|
+
<div className="flex flex-wrap gap-2 px-4 pb-3">
|
|
56
|
+
{reply.actions.map((action) => (
|
|
57
|
+
<Button key={action.label} type="button" variant="secondary" className="!w-auto" onClick={action.onClick}>
|
|
58
|
+
{action.label}
|
|
59
|
+
</Button>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
)}
|
|
63
|
+
<div className="px-4 pb-4">
|
|
64
|
+
<MessageBody messageUid={reply.uid} messageVersion={0} title={`Message: ${reply.subject}`} content={{ kind: "html", html: reply.html }} />
|
|
65
|
+
</div>
|
|
66
|
+
</CardShell>
|
|
67
|
+
);
|
|
68
|
+
}
|