@rapidmx/web-client 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +381 -373
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -480
- package/apps/admin/distribution-lists/new/index.tsx +3 -1
- package/apps/admin/domains/[uid].tsx +166 -164
- package/apps/admin/domains/new/index.tsx +3 -1
- package/apps/admin/escrow-scopes/[uid].tsx +350 -348
- package/apps/admin/escrow-scopes/new/index.tsx +3 -1
- package/apps/admin/index.tsx +129 -124
- package/apps/admin/mailboxes/[uid].tsx +271 -224
- package/apps/admin/mailboxes/new/index.tsx +30 -28
- package/apps/admin/plugins/index.tsx +15 -15
- package/apps/admin/retention-policy/index.tsx +39 -39
- package/apps/admin/signing-certificates/index.tsx +343 -343
- package/apps/admin/transport-rules/new/index.tsx +3 -1
- package/apps/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/escrow/matters/new/index.tsx +3 -1
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/components/admin/layout/AdminShell.tsx +381 -378
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
- package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
- package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -444
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/EventEditor.tsx +5 -10
- package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
- package/apps/shared/components/calendar/EventModal.tsx +206 -206
- package/apps/shared/components/calendar/GuestInput.tsx +100 -0
- package/apps/shared/components/calendar/MonthView.tsx +185 -185
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
- package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/calendar/eventForm.ts +2 -2
- package/apps/shared/components/calendar/eventFormat.ts +49 -22
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
- package/apps/shared/components/contacts/ContactForm.tsx +383 -383
- package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
- package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +218 -216
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +461 -480
- package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
- package/apps/shared/components/layout/UserMenu.tsx +439 -424
- package/apps/shared/components/mail/ConversationList.tsx +323 -323
- package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -491
- package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +240 -227
- package/apps/shared/components/mail/MenuButton.tsx +31 -11
- package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1559
- package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
- package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
- package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +161 -134
- package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
- package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
- package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
- package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
- package/apps/shared/components/mail/reading/messageExport.ts +59 -0
- package/apps/shared/components/mail/reading/printMessage.ts +99 -0
- package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -125
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
- package/apps/shared/components/settings/layout/SettingsShell.tsx +257 -254
- package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
- package/apps/shared/components/tasks/layout/TasksShell.tsx +219 -217
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
- package/apps/shared/keyboard/dispatch.ts +124 -124
- package/apps/shared/keyboard/format.ts +89 -89
- package/apps/shared/keyboard/keymap.ts +114 -114
- package/apps/shared/keyboard/registry.ts +65 -65
- package/apps/shared/keyboard/targets.ts +79 -79
- package/apps/shared/mail/folderCounts.ts +17 -0
- package/apps/shared/mail/folderOfType.ts +49 -0
- package/apps/shared/mail/folderTree.ts +143 -143
- package/apps/shared/mail/listAllPages.ts +39 -39
- package/apps/shared/mail/newMailNotifications.ts +171 -171
- package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
- package/apps/shared/mail/outbox/sendJob.ts +19 -1
- package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
- package/apps/shared/mail/permanentDelete.ts +160 -0
- package/apps/shared/mail/primaryMailbox.ts +71 -0
- package/apps/shared/mail/reportNotices.ts +66 -0
- package/apps/shared/mail/senderBlocking.ts +141 -0
- package/apps/shared/mail/useCollapsedSections.ts +146 -0
- package/apps/shared/mail/useMailConnection.ts +205 -203
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
- package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
- package/apps/shared/mail/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +178 -178
- package/apps/shared/mail/usePermanentDelete.tsx +160 -0
- package/apps/shared/navigation/frameContext.tsx +1 -1
- package/apps/shared/navigation/index.ts +43 -0
- package/apps/shared/navigation/pageTitle.ts +19 -0
- package/apps/shared/notifications/store.ts +560 -560
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
- package/apps/shared/search/crossMailboxSearch.ts +169 -0
- package/apps/shared/search/localIndexBuilder.ts +481 -481
- package/apps/shared/signing/enrollmentStorage.ts +33 -33
- package/apps/shared/signing/enrollmentTracker.ts +385 -385
- package/apps/shared/signing/enrollmentView.ts +251 -251
- package/apps/shared/signing/useNow.ts +19 -19
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
- package/apps/shared/styles/app.css +396 -386
- package/apps/www/_shell.tsx +82 -0
- package/apps/www/calendar/index.tsx +581 -571
- package/apps/www/contacts/[uid].tsx +112 -109
- package/apps/www/contacts/index.tsx +14 -3
- package/apps/www/index.tsx +2962 -2479
- package/apps/www/messages/[uid].tsx +139 -135
- package/apps/www/settings/appearance/index.tsx +5 -2
- package/apps/www/settings/auto-reply/index.tsx +136 -133
- package/apps/www/settings/blocked-senders/index.tsx +303 -0
- package/apps/www/settings/encryption/index.tsx +1290 -1287
- package/apps/www/settings/filters/[uid].tsx +179 -176
- package/apps/www/settings/filters/index.tsx +105 -102
- package/apps/www/settings/filters/new/index.tsx +165 -140
- package/apps/www/settings/labels/index.tsx +207 -204
- package/apps/www/settings/privacy/index.tsx +495 -492
- package/apps/www/settings/profile/index.tsx +251 -248
- package/apps/www/settings/read-receipts/index.tsx +150 -147
- package/apps/www/settings/sharing/index.tsx +259 -256
- package/apps/www/settings/signatures/[uid].tsx +175 -172
- package/apps/www/settings/signatures/index.tsx +91 -88
- package/apps/www/settings/signatures/new/index.tsx +138 -135
- package/apps/www/tasks/index.tsx +654 -649
- package/dist/apps/admin/data-requests/index.js +1 -1
- package/dist/apps/admin/distribution-lists/new/index.js +3 -1
- package/dist/apps/admin/domains/[uid].js +3 -1
- package/dist/apps/admin/domains/new/index.js +3 -1
- package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
- package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
- package/dist/apps/admin/index.js +5 -2
- package/dist/apps/admin/mailboxes/[uid].js +15 -3
- package/dist/apps/admin/mailboxes/new/index.js +3 -1
- package/dist/apps/admin/transport-rules/new/index.js +3 -1
- package/dist/apps/escrow/matters/new/index.js +3 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +6 -3
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
- package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
- package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
- package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
- package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
- package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
- package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +5 -4
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +7 -5
- package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
- package/dist/apps/shared/components/layout/AppShell.js +24 -33
- package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
- package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
- package/dist/apps/shared/components/layout/UserMenu.js +6 -5
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
- package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
- package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +14 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -13
- package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
- package/dist/apps/shared/components/mail/MenuButton.js +15 -11
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +16 -7
- package/dist/apps/shared/components/mail/layout/MailShell.js +78 -65
- package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
- package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
- package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
- package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
- package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
- package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +10 -7
- package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +7 -5
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
- package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
- package/dist/apps/shared/mail/folderCounts.js +12 -0
- package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
- package/dist/apps/shared/mail/folderOfType.js +40 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
- package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
- package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
- package/dist/apps/shared/mail/permanentDelete.js +127 -0
- package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
- package/dist/apps/shared/mail/primaryMailbox.js +61 -0
- package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
- package/dist/apps/shared/mail/reportNotices.js +47 -0
- package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
- package/dist/apps/shared/mail/senderBlocking.js +92 -0
- package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
- package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
- package/dist/apps/shared/mail/useMailConnection.js +3 -1
- package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
- package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
- package/dist/apps/shared/navigation/index.d.ts +16 -0
- package/dist/apps/shared/navigation/index.js +34 -0
- package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
- package/dist/apps/shared/navigation/pageTitle.js +12 -0
- package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
- package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
- package/dist/apps/shared/styles/app.css +396 -386
- package/dist/apps/www/_shell.d.ts +26 -0
- package/dist/apps/www/_shell.js +31 -0
- package/dist/apps/www/calendar/index.d.ts +7 -2
- package/dist/apps/www/calendar/index.js +7 -4
- package/dist/apps/www/contacts/[uid].d.ts +14 -3
- package/dist/apps/www/contacts/[uid].js +5 -3
- package/dist/apps/www/contacts/index.d.ts +7 -2
- package/dist/apps/www/contacts/index.js +8 -4
- package/dist/apps/www/index.d.ts +7 -2
- package/dist/apps/www/index.js +542 -207
- package/dist/apps/www/messages/[uid].d.ts +14 -3
- package/dist/apps/www/messages/[uid].js +7 -5
- package/dist/apps/www/settings/appearance/index.d.ts +8 -2
- package/dist/apps/www/settings/appearance/index.js +4 -2
- package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
- package/dist/apps/www/settings/auto-reply/index.js +4 -2
- package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
- package/dist/apps/www/settings/blocked-senders/index.js +134 -0
- package/dist/apps/www/settings/encryption/index.d.ts +7 -2
- package/dist/apps/www/settings/encryption/index.js +4 -2
- package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
- package/dist/apps/www/settings/filters/[uid].js +4 -2
- package/dist/apps/www/settings/filters/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/index.js +4 -2
- package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/new/index.js +28 -5
- package/dist/apps/www/settings/labels/index.d.ts +7 -2
- package/dist/apps/www/settings/labels/index.js +4 -2
- package/dist/apps/www/settings/privacy/index.d.ts +7 -2
- package/dist/apps/www/settings/privacy/index.js +4 -2
- package/dist/apps/www/settings/profile/index.d.ts +7 -2
- package/dist/apps/www/settings/profile/index.js +4 -2
- package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
- package/dist/apps/www/settings/read-receipts/index.js +4 -2
- package/dist/apps/www/settings/sharing/index.d.ts +7 -2
- package/dist/apps/www/settings/sharing/index.js +4 -2
- package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
- package/dist/apps/www/settings/signatures/[uid].js +4 -2
- package/dist/apps/www/settings/signatures/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/index.js +4 -2
- package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/new/index.js +5 -3
- package/dist/apps/www/tasks/index.d.ts +7 -2
- package/dist/apps/www/tasks/index.js +5 -3
- package/package.json +4 -2
- package/apps/shared/navigation/AppRouter.tsx +0 -300
- package/apps/shared/navigation/idle.ts +0 -45
- package/apps/shared/navigation/routerContext.tsx +0 -83
- package/apps/shared/navigation/routes.ts +0 -72
- package/apps/www/_routedPage.tsx +0 -24
- package/apps/www/_routes.ts +0 -37
- package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
- package/dist/apps/shared/navigation/AppRouter.js +0 -242
- package/dist/apps/shared/navigation/idle.d.ts +0 -14
- package/dist/apps/shared/navigation/idle.js +0 -43
- package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
- package/dist/apps/shared/navigation/routerContext.js +0 -56
- package/dist/apps/shared/navigation/routes.d.ts +0 -32
- package/dist/apps/shared/navigation/routes.js +0 -37
- package/dist/apps/www/_routedPage.d.ts +0 -12
- package/dist/apps/www/_routedPage.js +0 -19
- package/dist/apps/www/_routes.d.ts +0 -11
- package/dist/apps/www/_routes.js +0 -31
package/README.md
CHANGED
|
@@ -1,373 +1,381 @@
|
|
|
1
|
-
# RapidMX: Web Client
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@rapidmx/web-client)
|
|
4
|
-
|
|
5
|
-
RapidMX's webmail (`apps/www`), admin console (`apps/admin`) and escrow console (`apps/escrow`) React UI. The pages are
|
|
6
|
-
served and hydrated by [`rapidmx/server`](https://github.com/RapidMX/server) through `@rapidrest/react`'s file-convention
|
|
7
|
-
routes, and `@rapidmx/electron-client` reuses the same components. Platform-agnostic API clients, hooks and generic UI
|
|
8
|
-
primitives live in [`@rapidmx/react-shared`](https://github.com/RapidMX/react-shared).
|
|
9
|
-
|
|
10
|
-
## Package layout
|
|
11
|
-
|
|
12
|
-
The package ships the TSX sources (`apps/`) and a compiled mirror (`dist/apps/`, JavaScript plus `.d.ts` declarations).
|
|
13
|
-
There is no root export. Every module is its own subpath, mapped by `package.json`'s `exports` from
|
|
14
|
-
`@rapidmx/web-client/<path>.js` to `dist/apps/<path>.js`:
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
import SettingsShell from "@rapidmx/web-client/shared/components/settings/layout/SettingsShell.js";
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
`@rapidmx/web-client/shared/styles/app.css` is the Tailwind entry point and design tokens.
|
|
21
|
-
|
|
22
|
-
## Navigation without page loads
|
|
23
|
-
|
|
24
|
-
`@rapidrest/react`
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
| |
|
|
79
|
-
| |
|
|
80
|
-
| | `Ctrl+
|
|
81
|
-
| | `
|
|
82
|
-
| |
|
|
83
|
-
| | `
|
|
84
|
-
| | `
|
|
85
|
-
| |
|
|
86
|
-
|
|
|
87
|
-
| | `mod+
|
|
88
|
-
| | `
|
|
89
|
-
| | `
|
|
90
|
-
|
|
|
91
|
-
| | `
|
|
92
|
-
| | `
|
|
93
|
-
| |
|
|
94
|
-
|
|
|
95
|
-
| |
|
|
96
|
-
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
- **
|
|
117
|
-
|
|
118
|
-
- **
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
`
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- **
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
**
|
|
203
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
The
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
background
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
- **
|
|
276
|
-
|
|
277
|
-
the
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
**
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
`
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
1
|
+
# RapidMX: Web Client
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@rapidmx/web-client)
|
|
4
|
+
|
|
5
|
+
RapidMX's webmail (`apps/www`), admin console (`apps/admin`) and escrow console (`apps/escrow`) React UI. The pages are
|
|
6
|
+
served and hydrated by [`rapidmx/server`](https://github.com/RapidMX/server) through `@rapidrest/react`'s file-convention
|
|
7
|
+
routes, and `@rapidmx/electron-client` reuses the same components. Platform-agnostic API clients, hooks and generic UI
|
|
8
|
+
primitives live in [`@rapidmx/react-shared`](https://github.com/RapidMX/react-shared).
|
|
9
|
+
|
|
10
|
+
## Package layout
|
|
11
|
+
|
|
12
|
+
The package ships the TSX sources (`apps/`) and a compiled mirror (`dist/apps/`, JavaScript plus `.d.ts` declarations).
|
|
13
|
+
There is no root export. Every module is its own subpath, mapped by `package.json`'s `exports` from
|
|
14
|
+
`@rapidmx/web-client/<path>.js` to `dist/apps/<path>.js`:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import SettingsShell from "@rapidmx/web-client/shared/components/settings/layout/SettingsShell.js";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`@rapidmx/web-client/shared/styles/app.css` is the Tailwind entry point and design tokens.
|
|
21
|
+
|
|
22
|
+
## Navigation without page loads
|
|
23
|
+
|
|
24
|
+
All three apps use `@rapidrest/react`'s client-side router (`router = true` on the server's route, the client built with
|
|
25
|
+
`createViteConfig({ router })`; `@rapidrest/react` is a **peer dependency** here, `>=2.1.0 <3`, because the router's state lives
|
|
26
|
+
in a React context that this package must share with the server's one copy). The server still renders every URL, so first
|
|
27
|
+
loads, reloads and crawlers are unchanged; after the first load a click on an ordinary `<a href>` between two pages of an app
|
|
28
|
+
fetches the next page's props as JSON and swaps the page in. Back and forward work, the address bar always holds the real,
|
|
29
|
+
shareable URL, and anything the router cannot be sure of (a modified click, `target`, `download`, `data-router-ignore`, another
|
|
30
|
+
app's page - the consoles, plugin pages, other sites - or a failure) is left to the browser as a page load.
|
|
31
|
+
|
|
32
|
+
The admin and escrow pages each render their own shell, so the router just swaps the page. The webmail (`apps/www`) has an **app
|
|
33
|
+
shell**, `apps/www/_shell.tsx` (the router's persistent client layout), which the server renders around every page and the
|
|
34
|
+
browser hydrates with it as one root:
|
|
35
|
+
|
|
36
|
+
- the one `AppChrome` (the app rail, header, user menu, impersonation banner, compose windows in progress, the unlock prompt,
|
|
37
|
+
the idle-key timer, the mail connection and the notification pop-ups) stays mounted while only the page inside it is replaced.
|
|
38
|
+
A page's own shell (`MailShell`, `CalendarShell`, ...) still renders `AppShell`; inside the frame that is only its children;
|
|
39
|
+
- the shell gets the current page's props - `WwwRoute.fetchProps()`'s, the same for every page - so the rail's highlighted app
|
|
40
|
+
(from the route), the branding, the plugin navigation and the impersonation banner follow the server on every navigation. That is
|
|
41
|
+
one request to `fetchProps()` per page change (warm for most, since the router prefetches a plain link on hover and the four rail
|
|
42
|
+
pages' code when the browser is idle);
|
|
43
|
+
- **a folder is a shallow navigation.** `<a data-router-shallow>` (the Mail folder links), `navigate()` to the same path with another
|
|
44
|
+
query (`useNavigate()`, below) and `useSearchParams()`' setter keep the page instance and its state and only change the location;
|
|
45
|
+
- after a page change the router moves focus to the content region (`#app-content`), scrolls to the top and announces the page's
|
|
46
|
+
title in a polite live region (all set by the shell with `useNavigationEffects()`); the tab's title is each page's `title`
|
|
47
|
+
export (`pageTitle("Calendar")`: `Acme Mail: Calendar`), rendered by the server and sent with every navigation, with the unread
|
|
48
|
+
count kept in front of it; the content region is `aria-busy` while the next page loads.
|
|
49
|
+
|
|
50
|
+
Code that decides where to go uses `useNavigate()` from `shared/navigation/index.js` and reads the location with the library's
|
|
51
|
+
hooks:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { useLocation, useSearchParams } from "@rapidrest/react/client";
|
|
55
|
+
import { useNavigate } from "@rapidmx/web-client/shared/navigation/index.js";
|
|
56
|
+
|
|
57
|
+
const navigate = useNavigate(); // navigate("/contacts"); navigate("/?mailboxUid=a&folderUid=b") keeps the page (same path)
|
|
58
|
+
const { pathname, search, hash } = useLocation(); // the request's on the server and while hydrating; `hash` is empty there
|
|
59
|
+
const [params, setParams] = useSearchParams(); // setParams({ folderUid: "x" }) is shallow by default
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`navigate()` changes the page without a load when the URL is a page of the app, and is an ordinary navigation otherwise (outside a
|
|
63
|
+
router too). An admin or escrow page that leads to another page of its console after a save goes there with `useRouter().navigate`.
|
|
64
|
+
Plugin pages are not part of the router (they render their own `AppShell` chrome) and are opened with a page load.
|
|
65
|
+
|
|
66
|
+
The compose window, the reading pane, S/MIME and the emoji list are also chunks of their own, loaded on demand or fetched
|
|
67
|
+
when the browser is idle, so a page's first JavaScript is React and what the first screen draws.
|
|
68
|
+
|
|
69
|
+
## Keyboard shortcuts
|
|
70
|
+
|
|
71
|
+
One keyboard layer, `shared/keyboard/`, lives in the persistent app frame (`AppChrome`), so the shortcuts work in every view -
|
|
72
|
+
Mail, Calendar, Contacts, Tasks and Settings - and a page change only changes which of them are registered. `?` (or `Ctrl+/`)
|
|
73
|
+
opens a "Keyboard shortcuts" dialog, also reached from the account menu, that lists what is available *here* - the global
|
|
74
|
+
shortcuts plus those of the view on screen (and of an open compose window) - with the platform's own key names. `mod` below is
|
|
75
|
+
**Ctrl** on Windows and Linux and **Cmd** on a Mac; the navigation set is Ctrl+Shift on every platform (Cmd+Shift collides with the
|
|
76
|
+
browsers' own, so it is not offered).
|
|
77
|
+
|
|
78
|
+
| Where | Key | Does |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| Everywhere | `Ctrl+Shift+A` | Account (auth-server's account page; only offered when one is configured) |
|
|
81
|
+
| | `Ctrl+Shift+S` / `B` / `M` / `C` / `L` | Settings / Contacts / Mail / Calendar / To-Do |
|
|
82
|
+
| | `?` or `Ctrl+/` | Keyboard shortcuts |
|
|
83
|
+
| Mail | `Alt+N` | New message |
|
|
84
|
+
| | `mod+R` / `mod+Shift+R` / `mod+Shift+F` | Reply / Reply all / Forward the selected message |
|
|
85
|
+
| | `mod+D` or `Delete` | Delete the selected message or conversation (moves it to Deleted Items) |
|
|
86
|
+
| | `E` or `Backspace` | Archive |
|
|
87
|
+
| | `mod+Shift+V` | Move to folder |
|
|
88
|
+
| | `Ctrl+Q` / `mod+U` | Mark as read / unread (`Ctrl+Q` on a Mac too: `Cmd+Q` quits) |
|
|
89
|
+
| | `Insert` | Flag or unflag |
|
|
90
|
+
| | `Down` or `J` / `Up` or `K` | Next / previous message (conversation) |
|
|
91
|
+
| | `Ctrl+.` / `Ctrl+,` | Next / previous unread |
|
|
92
|
+
| | `Enter` / `Escape` | Open the selected message on its own page / clear the selection (leave select mode, clear the search) |
|
|
93
|
+
| | `/` or `mod+E` | Search |
|
|
94
|
+
| Compose window | `mod+Enter` | Send |
|
|
95
|
+
| | `mod+S` | Save draft |
|
|
96
|
+
| | `Escape` | Close, keeping the draft (the existing "keep draft / discard" question still applies) |
|
|
97
|
+
| | `Alt+N` | Another new message |
|
|
98
|
+
| Calendar | `Alt+N` | New event |
|
|
99
|
+
| | `T` | Today |
|
|
100
|
+
| | `Left` / `Right` (or `mod+Left` / `mod+Right`) | Previous / next period |
|
|
101
|
+
| | `Ctrl+Alt+1` / `2` / `3` / `4` | Day / work week / week / month |
|
|
102
|
+
| Contacts | `Alt+N` | New contact |
|
|
103
|
+
| | `/` or `mod+E` | Search |
|
|
104
|
+
| Tasks | `Alt+N` | New task (moves to the "Add a task" field) |
|
|
105
|
+
|
|
106
|
+
In the desktop client (`@rapidmx/electron-client`, which exposes `window.rapidmx`) `Ctrl+N` (`Cmd+N` on a Mac) also creates - a new
|
|
107
|
+
message, event, contact or task in the current view - and `Ctrl+Shift+T` also goes to Tasks; browsers keep both for themselves, which is
|
|
108
|
+
why the web client uses `Alt+N` and `Ctrl+Shift+L`.
|
|
109
|
+
|
|
110
|
+
How it behaves:
|
|
111
|
+
|
|
112
|
+
- **Views register only what they can do.** A view claims a shortcut with `useShortcut(SHORTCUTS.mail.reply, handler, { enabled })`
|
|
113
|
+
for as long as it is mounted and able to do it (Reply exists only while a message is selected, Archive not for Drafts or Outbox),
|
|
114
|
+
so there are no dead keys and the help dialog is always accurate. `SHORTCUTS` in `shared/keyboard/keymap.js` is the one key map;
|
|
115
|
+
handlers are looked up when the key is pressed. A handler that returns `false` declines the key.
|
|
116
|
+
- **Scopes.** `global`, the view's own (`mail`, `calendar`, `contacts`, `tasks`), `compose` (while focus is inside a compose window; it
|
|
117
|
+
beats the view behind it) and `dialog`: while a modal dialog (`aria-modal`) is open only its own shortcuts and its own Escape work.
|
|
118
|
+
- **Typing is never taken.** Bare keys (`J`, `E`, `?`, `Delete`) do nothing while focus is in a text field, select or the rich-text
|
|
119
|
+
editor, and caret keys with a modifier (word jumps) stay the field's. Chords with Ctrl/Alt/Cmd work from a field, except Option on
|
|
120
|
+
a Mac and Ctrl+Alt (AltGr) elsewhere, which type characters. Copy, Cut, Paste, Select all, Undo, Redo and Find are never
|
|
121
|
+
bound. Enter and Space are left to a focused button or link, and an open menu keeps its own keys. Events already
|
|
122
|
+
`defaultPrevented` (the editor's own bindings, a menu), IME composition and held-key repeats of one-shot actions are ignored, and
|
|
123
|
+
the browser's default is prevented only when a handler actually ran.
|
|
124
|
+
- **Layouts.** A key is matched on `event.key` (so it follows AZERTY or Dvorak) with `event.code` as the fallback when `event.key` is
|
|
125
|
+
not a Latin character - macOS Option+N, or a Cyrillic layout.
|
|
126
|
+
- **Hints.** Buttons that a shortcut also does carry `aria-keyshortcuts` and a tooltip such as "Reply (Ctrl+R)"; their accessible
|
|
127
|
+
names are unchanged (`useShortcutProps()` gives a control both).
|
|
128
|
+
|
|
129
|
+
Known limits: a browser keeps a few keys for itself (`Ctrl+N`, `Ctrl+T`, `Ctrl+W` are never delivered to a page, and some browsers claim
|
|
130
|
+
`Ctrl+Shift+A/B/C/M/S`); a message body is shown in a frame of its own, which hands the keys pressed in it on to the layer, so they work there too. In the compose body the editor's own bindings win over `Ctrl+Shift+S`, `B`
|
|
131
|
+
and `L` (strike-through, quote, align left).
|
|
132
|
+
|
|
133
|
+
## The reading pane
|
|
134
|
+
|
|
135
|
+
`shared/components/mail/` draws a message as a **card** and a conversation as a stack of them, under a **subject card** that stays pinned at the top
|
|
136
|
+
while the cards scroll (`MessageDetailPane`, `ConversationThreadPane`, and `reading/` for the pieces):
|
|
137
|
+
|
|
138
|
+
- **Subject card** - the conversation's (or the message's) subject as the page's `h1`, "N messages", a note when a very long thread was cut. In a
|
|
139
|
+
thread it is on screen at once, from what the list row already knows, with a skeleton card per message (up to three) until the messages arrive.
|
|
140
|
+
- **Message card** - rounded, bordered and lifted, in the app's own tokens (`bg-surface`, `text-text`, `border-border`), so it follows the theme, a
|
|
141
|
+
branding palette and the Appearance colours. A header row with the sender's initials, `Name <address>`, To/Cc/Bcc, the time and the actions (Reply,
|
|
142
|
+
Reply All, Forward, Archive, Move to, Labels, and "View original" when it applies); the security badge and every notice (signature, key change,
|
|
143
|
+
receipts, Outbox state); attachments; the body; a slim "You replied to this message." bar; and, on a single message and on the newest open one in a
|
|
144
|
+
thread, Reply and Forward at its foot. An older message of a thread is a collapsed card (sender, date, first line) that expands on click or Enter
|
|
145
|
+
(`aria-expanded`, `aria-controls`; the focus stays on the message's header button as it swaps between the two states).
|
|
146
|
+
- **Each card is exactly as tall as its message.** The body is shown at its natural height - no inner scrollbar, no dead space, no fixed height - and
|
|
147
|
+
follows it as images load and the width changes. A table or image wider than the card scrolls **inside** the card and never widens the pane. The
|
|
148
|
+
pane scrolls as a whole. Everything is on screen before the body: the header card, the message card's header and a skeleton where the body will be.
|
|
149
|
+
|
|
150
|
+
- **Encrypted messages** are themed card content in the same place as the body, never a frame: while the security state is being worked out, a skeleton;
|
|
151
|
+
locked (no unlocked keys on this device), a lock, "This message is encrypted", "Unlock your keys to read it" and an **Unlock** button that asks for the unlock
|
|
152
|
+
prompt and, once unlocked - by it or from anywhere - decrypts the message in place and moves the focus to it; unreadable with the keys in hand (not a recipient,
|
|
153
|
+
damaged), "This message can’t be decrypted" with the reason and no button; decrypted, the message like any other. The placeholder subject `[...]` reads
|
|
154
|
+
"Encrypted message", and list rows and collapsed cards with no preview say "Encrypted message" with a small lock.
|
|
155
|
+
|
|
156
|
+
**How a body is shown.** The server sanitizes a message's HTML when it ingests it (`GET /mail/messages/:id/content`); the client then treats what it
|
|
157
|
+
receives - and everything it recovers by decrypting or verifying, which the server never saw - as hostile. It is sanitized again with DOMPurify (scripts,
|
|
158
|
+
event handlers, `javascript:` URLs, every remote resource), stripped of forms, frames, media, plug-ins, SVG's active elements and every link that is not
|
|
159
|
+
`http(s)`/`mailto`/`tel`, and shown in a **sandboxed iframe without `allow-scripts`**, behind a Content-Security-Policy that allows no script, object, frame,
|
|
160
|
+
form or fetch (images are `data:` URIs and this server's own attachment URLs for inline `cid:` images; **remote images are never loaded**). `allow-same-origin`
|
|
161
|
+
is what lets the app measure the frame and adapt its colours, and it is safe only because nothing can run there: any one of the four layers (server
|
|
162
|
+
sanitizer, client sanitizer, CSP, sandbox) stops a script on its own - see `.claude/NOTES.md`, 2026-09-21, for the threat model and the hostile-mail corpus it
|
|
163
|
+
was tried against. Every link opens in a new tab without an opener. A message over 1.5 MB of markup or 20,000 elements is offered on its own page instead.
|
|
164
|
+
|
|
165
|
+
**Mail takes the theme.** A message follows the app's scheme wherever its author left the colours to the reader, and is shown as authored where the
|
|
166
|
+
author chose them. Plain-text mail and HTML with no colours at all (a typed reply) are drawn in the theme's surface and text colours, links in its link
|
|
167
|
+
colour. Where the HTML sets a colour: text on the theme's surface is kept if it reads (4.5:1) and adapted if it doesn't (black becomes white in a dark
|
|
168
|
+
theme, white becomes dark in a light one, a low-contrast navy is only lightened as far as it takes); anything on an authored, opaque background - a white table,
|
|
169
|
+
a banner, a gradient - is shown exactly as authored, except text nobody coloured, which gets black or white by contrast with that background, never the theme's
|
|
170
|
+
colour. A message that declares its own dark styles (a `color-scheme` meta or property, or a `prefers-color-scheme: dark` block) gets them when the theme is
|
|
171
|
+
dark. "View original" (the sun, in the card's header; the moon then goes back) shows just that message exactly as authored for the rest of the session; it is
|
|
172
|
+
offered only where adapting changes something. Known limits: a dark logo or PNG with a transparent background on a dark theme, text baked into images,
|
|
173
|
+
`mix-blend-mode` and SVG paint are left as they are.
|
|
174
|
+
|
|
175
|
+
Printing puts a message back to the colours it was written with for the print and adapts it again afterwards; the actions and the footer are not printed.
|
|
176
|
+
|
|
177
|
+
## Notifications, sending and the Outbox
|
|
178
|
+
|
|
179
|
+
**One pop-up system** (`shared/notifications/`). `AppChrome` mounts `NotificationCenter` once, right under the header row (a zero-height `sticky`
|
|
180
|
+
line that sticks just below the header - the title bar's or a branding header's height, published as `--rr-header-h` - so the stack starts below the
|
|
181
|
+
account menu and never covers it, the header's buttons or a compose window's title bar and Send button; its height is capped by `--rr-compose-top`), and
|
|
182
|
+
everything the app has to say goes through the framework-free store:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { notify, update, dismiss } from "@rapidmx/web-client/shared/notifications/store.js";
|
|
186
|
+
import { notifyApiError } from "@rapidmx/web-client/shared/notifications/apiErrors.js";
|
|
187
|
+
|
|
188
|
+
const id = notify({ kind: "error", title: "Couldn't archive the message", message: "The server said no.",
|
|
189
|
+
details: ["Status: 502 (api-1)"], actions: [{ label: "Retry", onClick: retry }], dedupeKey: "archive" });
|
|
190
|
+
update(id, { kind: "success", title: "Archived", actions: [] }); // resolve it
|
|
191
|
+
try { await archive(); } catch (err) { notifyApiError(err, "Couldn't archive the message"); }
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`kind` is `mail`, `info`, `success`, `warning` or `error`. At most three show at once (the rest queue); errors and anything with `actions` are sticky
|
|
195
|
+
until dismissed or resolved by `update()`/`dismiss()`, the others go after 5 to 10 seconds - clocks pause on hover, focus and a hidden tab; a repeated
|
|
196
|
+
`dedupeKey` is one pop-up with a count; errors are announced assertively (`role="alert"` inside an `aria-live="assertive"` region), the rest politely;
|
|
197
|
+
`details` is an expandable, monospace, copyable block. The last 30 (not new-mail ones) are kept in memory and `sessionStorage` and listed by "Recent
|
|
198
|
+
notifications" in the account menu. `notifyApiError()` turns an `ApiRequestError` into an error pop-up with the server's message (a `401` becomes
|
|
199
|
+
"Your session expired" with a Sign in button, anywhere in the app: the frame registers `setApiUnauthorizedObserver()`). The frame also catches unhandled
|
|
200
|
+
errors and rejections as one "Something went wrong", and says so, subtly, when the live connection has been down for more than ten seconds.
|
|
201
|
+
|
|
202
|
+
**Sending does not wait** (`shared/mail/outbox/`). Send validates what it can from what the window knows (a recipient, keys that must be unlocked, an
|
|
203
|
+
attachment that can't be signed - shown inline, the window stays open; an attachment still uploading is waited for), then closes the window at once and
|
|
204
|
+
hands a snapshot to `startSend()`: save, sign or encrypt in the browser if that applies, then `POST /mail/messages/:id/send` with `{ "background": true }`,
|
|
205
|
+
which answers `202 { status: "queued", message }` and relays in the background. Failures at any stage are a sticky "This message wasn't sent" with the
|
|
206
|
+
reason, technical details, **Retry** and **Open draft**; the server's later outcome arrives as `send-succeeded` / `send-retrying` / `send-failed` push
|
|
207
|
+
events. The Outbox row in the folder list is a pill with the count (optimistic at the click, then the server's), animated while anything is on its way and
|
|
208
|
+
red while one failed; rows in the Outbox list say what each message is doing.
|
|
209
|
+
|
|
210
|
+
**Encryption fails open.** A message is treated as unencrypted - its draft saves, Close and Send work, nothing about encryption is shown - unless you turned
|
|
211
|
+
encryption on (or replied to an encrypted message) or the encryption policy *was loaded* and encrypts for every recipient; a request that failed, is slow or
|
|
212
|
+
is pending never blocks anything (`compose/encryptionRequirement.ts`).
|
|
213
|
+
|
|
214
|
+
## The header, the footer and their variables
|
|
215
|
+
|
|
216
|
+
The admin console's Branding page sets a **header** and a **footer** as HTML. The server sanitizes them when they are saved and this client
|
|
217
|
+
sanitizes them again when it shows them: no scripts, styles, forms, inputs or SVG, no `on*` handlers, only `http`, `https` and `mailto` links.
|
|
218
|
+
|
|
219
|
+
**A custom header is the top of the app.** When one is set it replaces the app's own title bar (the app's name and the account menu) and the icon
|
|
220
|
+
at the top of the icon rail: the rail then starts with the app icons, and the header stays at the top of the window while a page scrolls. The
|
|
221
|
+
account menu moves into it. Two variables can be written in the *text* of the header or the footer:
|
|
222
|
+
|
|
223
|
+
| Variable | Replaced with |
|
|
224
|
+
| --- | --- |
|
|
225
|
+
| `{USER_MENU}` | The account menu: the avatar button and its drop-down (Account, Settings, notifications, keyboard shortcuts, Admin Console, Sign Out). |
|
|
226
|
+
| `{APP_TITLE}` | The name of the app on screen ("Mail", "Calendar", "Contacts", "Tasks", "Settings"), kept current as you move between apps without a page load. |
|
|
227
|
+
|
|
228
|
+
```html
|
|
229
|
+
<div style="display:flex; align-items:center; justify-content:space-between; padding:0.5rem 1rem">
|
|
230
|
+
<span>{APP_TITLE}</span>
|
|
231
|
+
<span>{USER_MENU}</span>
|
|
232
|
+
</div>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- A variable is matched exactly - upper case, in braces, no spaces - and only in text. One in an attribute (`title="{USER_MENU}"`), a comment,
|
|
236
|
+
a `<script>` or `<style>` is left as written or gone with them, and the author can't write the placeholders React fills in (`data-rr-slot` is
|
|
237
|
+
stripped).
|
|
238
|
+
- `{USER_MENU}` is replaced **once**, where it first appears in the header; if the header has none, at its first place in the footer (the menu then
|
|
239
|
+
opens upward); if neither has one, the menu sits in a small cell at the right end of the header, so the account menu, Sign Out and the shortcuts
|
|
240
|
+
help are never lost. A second or third copy is removed (two menus would mean duplicate ids and focus targets), and so is one inside a link or in
|
|
241
|
+
text that can't hold an element (`<title>`, `<textarea>`).
|
|
242
|
+
- `{APP_TITLE}` is replaced **everywhere** it appears, as text (never markup). It changes on client-side navigation, and works in the footer too.
|
|
243
|
+
- The menu is drawn into `<body>` with `position: fixed`, so a header with `overflow: hidden`, its own `z-index` or a stacking context can neither
|
|
244
|
+
clip it nor hide it behind the page. It follows its button when the window resizes or scrolls, and fits a 390 px window.
|
|
245
|
+
- Put `{USER_MENU}` where your CSS doesn't hide it: a header whose stylesheet hides its `<nav>` on phones should keep the variable outside the nav.
|
|
246
|
+
- Focus order is document order: the menu button is reached after the text before it and before what follows it.
|
|
247
|
+
- Pages that don't use a shell (the booking pages) render `BrandingHeader` and `BrandingFooter`, which have no menu: `{USER_MENU}` renders nothing
|
|
248
|
+
there and `{APP_TITLE}` is the `appTitle` prop, if any. The variable names are `USER_MENU_VARIABLE` and `APP_TITLE_VARIABLE` in
|
|
249
|
+
`shared/components/layout/BrandingChrome.js`.
|
|
250
|
+
|
|
251
|
+
The frame draws the header from the `branding` prop the server rendered the page with (`useBranding()`'s own fetch takes over when it answers), so
|
|
252
|
+
the frame is the right shape - no title bar, no rail icon - from the first paint, and the icon of a frame without a custom header sits flush with the
|
|
253
|
+
top of the window (`RailIcon`: it is measured on a canvas and raised by whatever empty margin the image file has above its artwork).
|
|
254
|
+
|
|
255
|
+
## Appearance
|
|
256
|
+
|
|
257
|
+
**Settings > Appearance** (`/settings/appearance`) lets each user choose the colour scheme (System, Light or Dark), four theme colours (primary,
|
|
258
|
+
accent, surface and text - each a colour picker with a hex field and its own "Reset"), and a background (none, a colour, or an uploaded picture with
|
|
259
|
+
dim 0-80%, blur 0-20 px and fit cover, contain or tile). Every change is applied to the real app in the same frame - the page you are on is the preview -
|
|
260
|
+
and saved in the background (coalesced, only what changed); a save that fails puts the change back and says so. A picture is checked before it is
|
|
261
|
+
uploaded (PNG, JPEG, WebP or AVIF, up to 8 MB). Text on the surface, and text on an accent button, under 4.5:1 contrast gets a warning, never a block.
|
|
262
|
+
"Reset all" returns everything to the defaults and removes the picture.
|
|
263
|
+
|
|
264
|
+
**How it reaches the app.** `AppearanceProvider` (mounted by `AppChrome`, `AdminShell` and `EscrowShell`) owns the preferences and writes one
|
|
265
|
+
`<style id="rr-appearance">` into `<head>` (`appearance/theme.js`), sets `<html data-theme="light|dark">` (for System, the operating system's scheme,
|
|
266
|
+
live) and remembers the result in `localStorage` (`rapidmx-appearance`). The server's `_layout.tsx` renders the same stylesheet from the `appearance`
|
|
267
|
+
page prop and a tiny inline script (`APPEARANCE_BOOT_SCRIPT`) applies the browser's copy before the first paint, so there is no flash on a load and none
|
|
268
|
+
on a client-side navigation. The server caches a rendered page for up to a minute, so whichever copy - the page's or the browser's - was changed last wins,
|
|
269
|
+
and the server is asked once the page is up.
|
|
270
|
+
|
|
271
|
+
- **Precedence:** the app's defaults < the deployment's branding stylesheet < the user's choices. A user's colours are `--rr-color-*` custom properties
|
|
272
|
+
declared `!important` on `<html>`, so no branding rule can beat them; the branding stylesheet's own rules (say a header's background) still decide their
|
|
273
|
+
own elements, exactly as before. Only the colours a user chose are set: primary and accent get their full scales (darker and lighter steps mixed from the
|
|
274
|
+
one colour) and readable text colours, surface and text get their alt, border and muted derivatives.
|
|
275
|
+
- **The background** is two fixed pseudo-elements of `<html>` (`::before` the picture or colour, `::after` the dim, the scheme's surface colour at the
|
|
276
|
+
chosen opacity), behind everything, with no markup, no layout shift and nothing to repaint while a list scrolls. With a background set the icon rail, the
|
|
277
|
+
header and the content region become translucent panels (`--color-surface` and `--color-surface-alt` are the surface colour at an alpha, chosen so text
|
|
278
|
+
and muted text stay at least 4.5:1 against the worst place the picture can be - the picture's lightest and darkest twentieth, measured on a canvas -
|
|
279
|
+
more see-through for a plain photo or a high dim, more opaque for a busy one); menus, dialogs, drawers and pop-ups take the opaque colours back. Without a
|
|
280
|
+
background nothing changes.
|
|
281
|
+
- **Hooks:** `useResolvedTheme()` (`appearance/resolvedTheme.js`) is `"light"` or `"dark"`, live, needing no provider; `useAppearance()` gives
|
|
282
|
+
`{ prefs, resolved, setPrefs(patch), uploadBackground(file), removeBackground(), reset(), saving, error, ... }`.
|
|
283
|
+
- **API** (`@rapidmx/react-shared/appearance/preferencesApi.js`): `GET/PUT /mail/preferences/appearance` (a merge), `POST/DELETE
|
|
284
|
+
/mail/preferences/appearance/background`, `GET .../background/:version`; a change is also pushed as `AppearancePreferences...` `update` events on
|
|
285
|
+
the user's own uid channel.
|
|
286
|
+
|
|
287
|
+
## Toolbars, folders and the signing certificate
|
|
288
|
+
|
|
289
|
+
**Toolbars that fit (`components/layout/ResponsiveToolbar.tsx`).** A bar measures its own width (`ResizeObserver`) and lays itself out from a table of actions (`ToolbarAction`: label,
|
|
290
|
+
icon, group, `rank` - the lowest goes into "More" first - and `essential`): captions under the icons while everything fits; else icons only (each keeps a tooltip and, for New contact, its
|
|
291
|
+
shortcut's `title` and `aria-keyshortcuts`); else as many icons as fit and the rest in a portalled `role="menu"` behind a **More** button. `layoutToolbar(actions, width)` is the pure
|
|
292
|
+
function behind it (`width === undefined` - server render, no ResizeObserver - shows everything), and the bar is `overflow-x-clip` so it can never paint over the pane beside it. Contacts
|
|
293
|
+
uses it; the panes around it follow one rule: a list column of `clamp(16rem, 32vw, 26rem)`, a detail pane that keeps the rest, each column scrolling by itself (`md:h-[calc(100dvh - var(--rr-header-h))]` on the
|
|
294
|
+
page root - the frame's own height is open-ended), the side menu a drawer below `lg`, and no empty shell column for a single mailbox.
|
|
295
|
+
|
|
296
|
+
**The folder tree (`mail/folderTree.ts`).** `FOLDER_ORDER` (Inbox, Drafts, Outbox, Sent Items, Deleted Items, Junk, Archive, then custom), `upsertFolder()`, `removeFolder()` and
|
|
297
|
+
`reconcileFolders()` return the same array when nothing changed (everything that reads the tree starts over when it is a new one), never touch the counts (the overlay in `folderCounts.ts`
|
|
298
|
+
owns them) and never remove on a listing (deletions arrive as events). `useMailConnection()` files what `Folder` create / update / delete events say and what every counts read-back lists
|
|
299
|
+
(`useFolderCounts(mailboxes, folders, onFoldersListed)`), so a folder is found by an event, a message in it, a send outcome, the poll, a focus or a reconnect, whichever comes first. `folderRows()`
|
|
300
|
+
is the one place a folder that has not arrived is drawn - a muted Outbox / Sent Items placeholder while a message is on its way - so it can never be drawn twice. Push channels are ordered inboxes,
|
|
301
|
+
mailbox uids, then the other folders in sidebar order (`pushChannelsFor()`).
|
|
302
|
+
|
|
303
|
+
**The signing certificate (`signing/`).** `enrollmentTracker.ts` follows one enrollment per mailbox for everybody who wants to show or react to it: a first read, then 15 s, 30 s and every 60 s
|
|
304
|
+
while pending - only while the page is visible (a hidden tab asks nothing; coming back or a focus asks at once and restarts the backoff), stopping when it ends; `checkEnrollmentNow()` is the
|
|
305
|
+
"Check status" (a 10 s cooldown, the server's 429 honoured, a plain read on a server without the check endpoint); a pending enrollment stays followed after the page that started it is left.
|
|
306
|
+
`useSigningEnrollmentWatcher()` (in `AppChrome`) seeds it for the mailboxes you own and raises the "issued" / "failed" pop-up. `enrollmentView.ts` holds the wording, the steps, the percentage
|
|
307
|
+
and the expiry maths; `SigningCertificateCard` draws them. The account menu's **Theme** row is `ThemeSwitch`, over `useAppearance().setPrefs({ mode })`.
|
|
308
|
+
|
|
309
|
+
## Plugin UI surface
|
|
310
|
+
|
|
311
|
+
Server plugins can ship their own pages (see the plugin manifest's `ui` field in `@rapidmx/restapi`). The server builds
|
|
312
|
+
them together with this package, so they share one React, one `@rapidmx/react-shared` state and one stylesheet. The
|
|
313
|
+
modules below are the **supported surface for plugin pages**. Anything else under `apps/` is internal and may change in
|
|
314
|
+
any release.
|
|
315
|
+
|
|
316
|
+
### Shells
|
|
317
|
+
|
|
318
|
+
| Import | Use |
|
|
319
|
+
| --- | --- |
|
|
320
|
+
| `shared/components/layout/AppShell.js` | Chrome for webmail apps: app rail, header, user menu, impersonation banner, compose and unlock providers. `active` is a core app or the plugin's `appRail` item id. The user menu shows an "Admin Console" item to an administrator even when their session isn't elevated (it asks auth-server for the user's own roles, using the `trustedRoles` page prop) and, in Mail, the new-mail pop-up switch. |
|
|
321
|
+
| `shared/components/settings/layout/SettingsShell.js` | Settings chrome with the section list and mailbox switcher. `active` is the plugin's `settingsSections` item id. `useSettingsShell()` gives the selected `mailboxUid` and the accessible `mailboxes`. |
|
|
322
|
+
| `shared/components/admin/layout/AdminShell.js` | Admin console chrome, gated on administrator access - an administrator whose session isn't elevated is sent to auth-server's `/auth/elevate` page and returned. `active` is the plugin's `adminNav` item id. |
|
|
323
|
+
| `shared/components/layout/BrandingChrome.js` | `BrandingHeader` and `BrandingFooter`, for pages that don't use a shell, such as public pages. |
|
|
324
|
+
| `shared/plugins/pluginNav.js` | The `PluginNav`, `PluginUiNavItem` and `PluginNavProps` types. |
|
|
325
|
+
|
|
326
|
+
Every www and admin page receives a `pluginNav` prop from the server. It lists the settings sections, admin sections and
|
|
327
|
+
app rail entries of every enabled plugin whose UI built. The shells append those entries after their own, with a generic
|
|
328
|
+
icon. An entry whose id matches a core entry is skipped, and so is one whose `href` isn't a same-origin path. Pass the
|
|
329
|
+
page props straight to the shell so the navigation shows:
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
import React from "react";
|
|
333
|
+
import SettingsShell, {
|
|
334
|
+
SettingsShellProps,
|
|
335
|
+
useSettingsShell,
|
|
336
|
+
} from "@rapidmx/web-client/shared/components/settings/layout/SettingsShell.js";
|
|
337
|
+
|
|
338
|
+
export default function RemindersSettingsPage(props: Omit<SettingsShellProps, "active">) {
|
|
339
|
+
return (
|
|
340
|
+
<SettingsShell {...props} active="reminders">
|
|
341
|
+
<RemindersSettings />
|
|
342
|
+
</SettingsShell>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function RemindersSettings() {
|
|
347
|
+
const { mailboxUid } = useSettingsShell();
|
|
348
|
+
return <p>Settings for {mailboxUid}</p>;
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Public and escrow pages get no `pluginNav`.
|
|
353
|
+
|
|
354
|
+
### From `@rapidmx/react-shared`
|
|
355
|
+
|
|
356
|
+
Plugin pages import these directly from `@rapidmx/react-shared`, which the server resolves to the same copy the shells
|
|
357
|
+
use:
|
|
358
|
+
|
|
359
|
+
- `branding/useBranding.js`: `useBranding()`, for the branding and icon of pages outside a shell;
|
|
360
|
+
- `auth/session.js`: `useRedirectIfUnauthenticated()`, already called by every shell;
|
|
361
|
+
- `util/api.js`: `apiFetch()` and `ApiRequestError`, for calling the plugin's own API routes;
|
|
362
|
+
- `mail/mailApi.js`: mailboxes and folders, such as `listMailboxes()` and `listFolders()`;
|
|
363
|
+
- `components/buttons/Button.js`, `components/feedback/Alert.js` and `components/feedback/Skeleton.js`;
|
|
364
|
+
- `components/forms/FormField.js`;
|
|
365
|
+
- `components/overlays/Modal.js` and `components/overlays/Drawer.js`;
|
|
366
|
+
- `components/pickers/MiniDatePicker.js`.
|
|
367
|
+
|
|
368
|
+
## Uninstalling a plugin with its data
|
|
369
|
+
|
|
370
|
+
The admin console's Plugins page (`PluginsManager`, also embedded in the setup wizard) uninstalls a plugin through a dialog with an unchecked **Also delete all data this plugin stored** box. Ticking it lists what will be deleted (collections and tables, saved settings, cached package and pages, whatever the plugin cleans up itself), says **This can't be undone**, turns the confirm button into the red **Uninstall and delete data** and keeps it disabled until the plugin's display name is typed (case and surrounding spaces ignored). The dialog is a `Modal` (focus moves in and stays in, Escape closes, the checkbox is described by the list) and a `<form>`, so Enter in the name field confirms once it matches; at 390 px it keeps the modal's 20 px gutters and the list wraps.
|
|
371
|
+
|
|
372
|
+
The request is `removePlugin(uid, { purgeData: true })` (`@rapidmx/react-shared/admin/pluginsApi.js`); the server accepts it only from an elevated administrator, and an `api-104` answer is shown as a request to reload or sign in again. Deletion happens on the servers after the last copy stops running the plugin, so `GET /api/system/plugins/status` carries each deletion as `purges` and the page lists the uninstalled plugin with its state - *Uninstalled - data will be deleted after servers restart* (and how many servers still run it), *Data deleted <date>*, or *Data deletion failed: <reason>* with the failed steps and a Retry button. It reads the status every 5 seconds while a deletion is waiting or running, and raises an `apps/shared/notifications` pop-up when one that was under way is deleted or fails, and a warning when adding a plugin cancels one. A plugin that is installed is never shown as uninstalled, whatever the server still lists about an earlier deletion.
|
|
373
|
+
|
|
374
|
+
## Development
|
|
375
|
+
|
|
376
|
+
```sh
|
|
377
|
+
yarn install
|
|
378
|
+
yarn test # vitest with coverage gates
|
|
379
|
+
yarn lint
|
|
380
|
+
yarn build # tsc into dist/apps
|
|
381
|
+
```
|