@rapidmx/web-client 0.14.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 -163
- 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/appearance/AppearanceForm.tsx +9 -1
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/calendar/bookingPlugin.ts +100 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +27 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +378 -346
- 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 +53 -4
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +64 -10
- 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/AppointmentQuickForm.tsx +327 -0
- package/apps/shared/components/calendar/DescriptionEditor.tsx +274 -0
- package/apps/shared/components/calendar/EventDescriptionView.tsx +70 -0
- package/apps/shared/components/calendar/EventDetails.tsx +401 -0
- package/apps/shared/components/calendar/EventEditor.tsx +635 -0
- package/apps/shared/components/calendar/EventExpandedForm.tsx +210 -0
- package/apps/shared/components/calendar/EventFormParts.tsx +591 -0
- package/apps/shared/components/calendar/EventModal.tsx +206 -946
- package/apps/shared/components/calendar/EventQuickForm.tsx +116 -0
- package/apps/shared/components/calendar/EventShell.tsx +202 -0
- package/apps/shared/components/calendar/FindATime.tsx +379 -0
- package/apps/shared/components/calendar/GuestInput.tsx +100 -0
- package/apps/shared/components/calendar/LazyDescriptionEditor.tsx +70 -0
- package/apps/shared/components/calendar/MonthView.tsx +185 -173
- package/apps/shared/components/calendar/QuickCreateFaces.tsx +87 -0
- package/apps/shared/components/calendar/QuickCreateTabs.tsx +81 -0
- package/apps/shared/components/calendar/QuickFaceFrame.tsx +81 -0
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -222
- package/apps/shared/components/calendar/RequestChangeForm.tsx +240 -0
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -143
- package/apps/shared/components/calendar/TaskQuickForm.tsx +289 -0
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -244
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/calendar/eventDialogFields.ts +86 -0
- package/apps/shared/components/calendar/eventForm.ts +108 -0
- package/apps/shared/components/calendar/eventFormat.ts +210 -0
- package/apps/shared/components/calendar/findTimeGrid.ts +90 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
- package/apps/shared/components/calendar/swipeNavigation.ts +14 -0
- 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 -372
- package/apps/shared/components/mail/ConversationList.tsx +323 -271
- package/apps/shared/components/mail/ConversationThreadPane.tsx +130 -5
- package/apps/shared/components/mail/InviteCard.tsx +387 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +236 -227
- package/apps/shared/components/mail/MessageDetailPane.tsx +1559 -1537
- package/apps/shared/components/mail/SwipeRow.tsx +120 -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 -1673
- 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/invite/InviteRowChip.tsx +104 -0
- package/apps/shared/components/mail/invite/InviteRsvpPopover.tsx +271 -0
- package/apps/shared/components/mail/invite/InviteTimeline.tsx +200 -0
- package/apps/shared/components/mail/invite/ProposeTimeForm.tsx +109 -0
- package/apps/shared/components/mail/invite/inviteFormat.ts +110 -0
- package/apps/shared/components/mail/invite/inviteStore.ts +114 -0
- package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +546 -490
- 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 -253
- package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
- package/apps/shared/components/tasks/layout/TasksShell.tsx +218 -217
- package/apps/shared/gestures/useEnterSlide.ts +54 -0
- package/apps/shared/gestures/useSwipe.ts +114 -0
- package/apps/shared/gestures/useSwipeSlide.ts +113 -0
- package/apps/shared/gestures/useWheelPaging.ts +107 -0
- 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 +3 -15
- 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 +2 -2
- package/apps/shared/navigation/AppRouter.tsx +300 -300
- package/apps/shared/navigation/routerContext.tsx +83 -83
- package/apps/shared/notifications/preferences.ts +36 -0
- package/apps/shared/notifications/store.ts +9 -0
- 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/_routes.ts +1 -0
- package/apps/www/calendar/index.tsx +578 -469
- package/apps/www/contacts/[uid].tsx +109 -109
- package/apps/www/contacts/index.tsx +6 -0
- package/apps/www/index.tsx +2847 -2332
- package/apps/www/messages/[uid].tsx +135 -108
- 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 -0
- 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/domains/[uid].js +4 -2
- package/dist/apps/admin/index.js +2 -1
- package/dist/apps/admin/mailboxes/[uid].js +12 -2
- package/dist/apps/shared/appearance/AppearanceForm.js +9 -1
- package/dist/apps/shared/calendar/bookingPlugin.d.ts +50 -0
- package/dist/apps/shared/calendar/bookingPlugin.js +57 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +23 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +7 -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 +28 -4
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +40 -6
- package/dist/apps/shared/components/calendar/AppointmentQuickForm.d.ts +47 -0
- package/dist/apps/shared/components/calendar/AppointmentQuickForm.js +135 -0
- package/dist/apps/shared/components/calendar/DescriptionEditor.d.ts +26 -0
- package/dist/apps/shared/components/calendar/DescriptionEditor.js +192 -0
- package/dist/apps/shared/components/calendar/EventDescriptionView.d.ts +14 -0
- package/dist/apps/shared/components/calendar/EventDescriptionView.js +39 -0
- package/dist/apps/shared/components/calendar/EventDetails.d.ts +33 -0
- package/dist/apps/shared/components/calendar/EventDetails.js +137 -0
- package/dist/apps/shared/components/calendar/EventEditor.d.ts +59 -0
- package/dist/apps/shared/components/calendar/EventEditor.js +544 -0
- package/dist/apps/shared/components/calendar/EventExpandedForm.d.ts +14 -0
- package/dist/apps/shared/components/calendar/EventExpandedForm.js +44 -0
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +70 -0
- package/dist/apps/shared/components/calendar/EventFormParts.js +191 -0
- package/dist/apps/shared/components/calendar/EventModal.d.ts +26 -13
- package/dist/apps/shared/components/calendar/EventModal.js +57 -459
- package/dist/apps/shared/components/calendar/EventQuickForm.d.ts +18 -0
- package/dist/apps/shared/components/calendar/EventQuickForm.js +25 -0
- package/dist/apps/shared/components/calendar/EventShell.d.ts +64 -0
- package/dist/apps/shared/components/calendar/EventShell.js +134 -0
- package/dist/apps/shared/components/calendar/FindATime.d.ts +18 -0
- package/dist/apps/shared/components/calendar/FindATime.js +190 -0
- 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/LazyDescriptionEditor.d.ts +8 -0
- package/dist/apps/shared/components/calendar/LazyDescriptionEditor.js +44 -0
- package/dist/apps/shared/components/calendar/MonthView.d.ts +5 -1
- package/dist/apps/shared/components/calendar/MonthView.js +11 -5
- package/dist/apps/shared/components/calendar/QuickCreateFaces.d.ts +30 -0
- package/dist/apps/shared/components/calendar/QuickCreateFaces.js +44 -0
- package/dist/apps/shared/components/calendar/QuickCreateTabs.d.ts +24 -0
- package/dist/apps/shared/components/calendar/QuickCreateTabs.js +46 -0
- package/dist/apps/shared/components/calendar/QuickFaceFrame.d.ts +25 -0
- package/dist/apps/shared/components/calendar/QuickFaceFrame.js +19 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +2 -2
- package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +36 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +127 -0
- package/dist/apps/shared/components/calendar/SplitDayView.d.ts +3 -2
- package/dist/apps/shared/components/calendar/SplitDayView.js +3 -2
- package/dist/apps/shared/components/calendar/TaskQuickForm.d.ts +40 -0
- package/dist/apps/shared/components/calendar/TaskQuickForm.js +101 -0
- package/dist/apps/shared/components/calendar/TimeGridView.d.ts +4 -2
- package/dist/apps/shared/components/calendar/TimeGridView.js +3 -2
- package/dist/apps/shared/components/calendar/eventDialogFields.d.ts +28 -0
- package/dist/apps/shared/components/calendar/eventDialogFields.js +59 -0
- package/dist/apps/shared/components/calendar/eventForm.d.ts +98 -0
- package/dist/apps/shared/components/calendar/eventForm.js +1 -0
- package/dist/apps/shared/components/calendar/eventFormat.d.ts +80 -0
- package/dist/apps/shared/components/calendar/eventFormat.js +190 -0
- package/dist/apps/shared/components/calendar/findTimeGrid.d.ts +30 -0
- package/dist/apps/shared/components/calendar/findTimeGrid.js +83 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -2
- package/dist/apps/shared/components/calendar/swipeNavigation.d.ts +9 -0
- package/dist/apps/shared/components/calendar/swipeNavigation.js +8 -0
- 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 +14 -9
- package/dist/apps/shared/components/layout/UserMenu.js +74 -26
- package/dist/apps/shared/components/mail/ConversationList.d.ts +12 -1
- package/dist/apps/shared/components/mail/ConversationList.js +21 -4
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
- package/dist/apps/shared/components/mail/InviteCard.d.ts +20 -0
- package/dist/apps/shared/components/mail/InviteCard.js +150 -0
- 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/MessageDetailPane.js +13 -2
- package/dist/apps/shared/components/mail/SwipeRow.d.ts +22 -0
- package/dist/apps/shared/components/mail/SwipeRow.js +71 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +16 -7
- 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/invite/InviteRowChip.d.ts +23 -0
- package/dist/apps/shared/components/mail/invite/InviteRowChip.js +46 -0
- package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.d.ts +20 -0
- package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.js +141 -0
- package/dist/apps/shared/components/mail/invite/InviteTimeline.d.ts +27 -0
- package/dist/apps/shared/components/mail/invite/InviteTimeline.js +120 -0
- package/dist/apps/shared/components/mail/invite/ProposeTimeForm.d.ts +17 -0
- package/dist/apps/shared/components/mail/invite/ProposeTimeForm.js +45 -0
- package/dist/apps/shared/components/mail/invite/inviteFormat.d.ts +28 -0
- package/dist/apps/shared/components/mail/invite/inviteFormat.js +94 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.d.ts +28 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.js +95 -0
- 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 +10 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +76 -46
- 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 +7 -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/gestures/useEnterSlide.d.ts +14 -0
- package/dist/apps/shared/gestures/useEnterSlide.js +47 -0
- package/dist/apps/shared/gestures/useSwipe.d.ts +45 -0
- package/dist/apps/shared/gestures/useSwipe.js +90 -0
- package/dist/apps/shared/gestures/useSwipeSlide.d.ts +40 -0
- package/dist/apps/shared/gestures/useSwipeSlide.js +95 -0
- package/dist/apps/shared/gestures/useWheelPaging.d.ts +32 -0
- package/dist/apps/shared/gestures/useWheelPaging.js +85 -0
- package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
- package/dist/apps/shared/mail/folderCounts.js +12 -0
- package/dist/apps/shared/mail/newMailNotifications.d.ts +3 -8
- package/dist/apps/shared/mail/newMailNotifications.js +3 -12
- 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/mail/useNewMailNotifications.js +3 -2
- package/dist/apps/shared/notifications/preferences.d.ts +13 -0
- package/dist/apps/shared/notifications/preferences.js +36 -0
- package/dist/apps/shared/notifications/store.d.ts +1 -0
- package/dist/apps/shared/notifications/store.js +7 -23
- 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/_routes.js +1 -0
- package/dist/apps/www/calendar/index.js +78 -7
- package/dist/apps/www/contacts/index.js +3 -1
- package/dist/apps/www/index.js +548 -210
- package/dist/apps/www/messages/[uid].js +13 -5
- package/dist/apps/www/settings/profile/index.d.ts +4 -0
- package/dist/apps/www/settings/profile/index.js +124 -0
- package/package.json +2 -2
|
@@ -1,143 +1,143 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import type { Folder } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
6
|
-
import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The sidebar's order of a mailbox's well-known folders; anything else (`user`) follows, alphabetically. Also the order push channels are asked
|
|
10
|
-
* for (`pushChannelsFor()`), so when the server's channel cap bites it is the folders lowest in this list that go without live updates.
|
|
11
|
-
*/
|
|
12
|
-
export const FOLDER_ORDER = ["inbox", "drafts", "outbox", "sent_items", "deleted_items", "junk", "archive"];
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see `CalendarShell`/`ContactsShell`/`TasksShell`), not
|
|
16
|
-
* Mail - `listFolders()` returns every well-known folder for the mailbox regardless of which app owns it, so Mail's own folder tree filters down
|
|
17
|
-
* to just the mail ones itself, or those other apps' folders leak into its sidebar.
|
|
18
|
-
*/
|
|
19
|
-
export const MAIL_FOLDER_TYPES = new Set([...FOLDER_ORDER, "user"]);
|
|
20
|
-
|
|
21
|
-
/** The well-known folders the server creates lazily around a first send: what the sidebar shows a placeholder for while a message is on its way. */
|
|
22
|
-
export const SEND_FOLDER_TYPES = ["outbox", "sent_items"] as const;
|
|
23
|
-
|
|
24
|
-
export function isMailFolder(folder: { type: string }): boolean {
|
|
25
|
-
return MAIL_FOLDER_TYPES.has(folder.type);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function sortKey(type: string): number {
|
|
29
|
-
const index = FOLDER_ORDER.indexOf(type);
|
|
30
|
-
return index === -1 ? FOLDER_ORDER.length : index;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Well-known folders in `FOLDER_ORDER`, then the rest by name. A new array; the input is left alone. */
|
|
34
|
-
export function sortFolders(folders: Folder[]): Folder[] {
|
|
35
|
-
return [...folders].sort((a, b) => sortKey(a.type) - sortKey(b.type) || a.name.localeCompare(b.name));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** What a sidebar row is: a folder that exists, or (`placeholder`) one that is expected to but has not arrived yet. */
|
|
39
|
-
export type FolderRow = { kind: "folder"; folder: Folder } | { kind: "placeholder"; type: string };
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The rows of one mailbox's sidebar section, in order: every folder that exists, and - while `sending` messages are on their way and the
|
|
43
|
-
* server has not made the folder yet - a placeholder for each of the Outbox and Sent Items that is missing. The one place a folder that has not
|
|
44
|
-
* arrived is drawn, so it can never appear twice: the moment the real folder is in `folders` its placeholder is not built.
|
|
45
|
-
*/
|
|
46
|
-
export function folderRows(folders: Folder[], sending: number): FolderRow[] {
|
|
47
|
-
const rows: FolderRow[] = sortFolders(folders).map((folder) => ({ kind: "folder", folder }));
|
|
48
|
-
if (sending > 0) {
|
|
49
|
-
for (const type of SEND_FOLDER_TYPES) {
|
|
50
|
-
if (!folders.some((folder) => folder.type === type)) {
|
|
51
|
-
rows.push({ kind: "placeholder", type });
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
rows.sort((a, b) => sortKey(rowType(a)) - sortKey(rowType(b)));
|
|
55
|
-
}
|
|
56
|
-
return rows;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function rowType(row: FolderRow): string {
|
|
60
|
-
return row.kind === "folder" ? row.folder.type : row.type;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** What a folder can be changed to by an event or a refetch: everything about it except the counts, which the counts overlay owns. */
|
|
64
|
-
export type FolderChange = Partial<Pick<Folder, "name" | "type" | "parentFolderUid" | "color">> & { uid: string; mailboxUid?: string };
|
|
65
|
-
|
|
66
|
-
const CHANGEABLE = ["name", "type", "parentFolderUid", "color"] as const;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Files `folder` in the tree: added under its mailbox when unknown (a type Mail does not list is ignored), else its name, type, parent and
|
|
70
|
-
* colour brought up to date (a folder that became a type Mail does not list leaves the tree). Returns `entries` itself - the same array - when
|
|
71
|
-
* nothing changed, because everything on screen that depends on the tree starts over when it is a new one.
|
|
72
|
-
*/
|
|
73
|
-
export function upsertFolder(entries: MailboxFolders[], folder: Folder | FolderChange): MailboxFolders[] {
|
|
74
|
-
let changed = false;
|
|
75
|
-
const next = entries.map((entry) => {
|
|
76
|
-
const existing = entry.folders.find((f) => f.uid === folder.uid);
|
|
77
|
-
if (!existing) {
|
|
78
|
-
// Only a whole folder can be added: an update for one that is not known has nothing to add it with (the refetch it causes will).
|
|
79
|
-
if (entry.mailbox.uid !== folder.mailboxUid || !isFullFolder(folder) || !isMailFolder(folder)) {
|
|
80
|
-
return entry;
|
|
81
|
-
}
|
|
82
|
-
// A well-known folder is one per mailbox - the server treats the oldest as the real one - so a second of the same type (made by a client that did
|
|
83
|
-
// not know it existed) is not a second row: the older of the two is the one shown.
|
|
84
|
-
const sameType = folder.type === "user" ? undefined : entry.folders.find((f) => f.type === folder.type);
|
|
85
|
-
if (sameType) {
|
|
86
|
-
if (!isOlder(folder, sameType)) {
|
|
87
|
-
return entry;
|
|
88
|
-
}
|
|
89
|
-
changed = true;
|
|
90
|
-
return { ...entry, folders: entry.folders.map((f) => (f === sameType ? folder : f)) };
|
|
91
|
-
}
|
|
92
|
-
changed = true;
|
|
93
|
-
return { ...entry, folders: [...entry.folders, folder] };
|
|
94
|
-
}
|
|
95
|
-
const patch: Partial<Folder> = {};
|
|
96
|
-
for (const key of CHANGEABLE) {
|
|
97
|
-
if (key in folder && folder[key] !== undefined && folder[key] !== existing[key]) {
|
|
98
|
-
(patch as Record<string, unknown>)[key] = folder[key];
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (Object.keys(patch).length === 0) {
|
|
102
|
-
return entry;
|
|
103
|
-
}
|
|
104
|
-
changed = true;
|
|
105
|
-
const updated = { ...existing, ...patch };
|
|
106
|
-
return { ...entry, folders: isMailFolder(updated) ? entry.folders.map((f) => (f === existing ? updated : f)) : entry.folders.filter((f) => f !== existing) };
|
|
107
|
-
});
|
|
108
|
-
return changed ? next : entries;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function isFullFolder(folder: Folder | FolderChange): folder is Folder {
|
|
112
|
-
return typeof folder.name === "string" && typeof folder.type === "string";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Whether `a` was created before `b`: only when both say when. */
|
|
116
|
-
function isOlder(a: Folder, b: Folder): boolean {
|
|
117
|
-
// (A folder from the network may lack the date: `Date.parse(undefined)` is NaN, which is "cannot tell".)
|
|
118
|
-
const first = Date.parse(a.dateCreated);
|
|
119
|
-
const second = Date.parse(b.dateCreated);
|
|
120
|
-
return Number.isFinite(first) && Number.isFinite(second) && first < second;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Takes a deleted folder out of the tree. The same array when it was not in it. */
|
|
124
|
-
export function removeFolder(entries: MailboxFolders[], folderUid: string): MailboxFolders[] {
|
|
125
|
-
if (!entries.some((entry) => entry.folders.some((folder) => folder.uid === folderUid))) {
|
|
126
|
-
return entries;
|
|
127
|
-
}
|
|
128
|
-
return entries.map((entry) => (entry.folders.some((folder) => folder.uid === folderUid) ? { ...entry, folders: entry.folders.filter((folder) => folder.uid !== folderUid) } : entry));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Brings one mailbox's part of the tree up to date with a listing the server just gave (`listFolders()`): folders it has that the tree lacks
|
|
133
|
-
* are added, changed names and types are taken. Nothing is removed - a listing that began before a folder was created here would take it out
|
|
134
|
-
* again, and deletions arrive as events - and the counts are not touched (see `useFolderCounts()`). The same array when there is nothing to do.
|
|
135
|
-
*/
|
|
136
|
-
export function reconcileFolders(entries: MailboxFolders[], listed: Folder[]): MailboxFolders[] {
|
|
137
|
-
return listed.reduce((tree, folder) => upsertFolder(tree, folder), entries);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Every folder uid in the tree. */
|
|
141
|
-
export function knownFolderUids(entries: MailboxFolders[]): Set<string> {
|
|
142
|
-
return new Set(entries.flatMap((entry) => entry.folders.map((folder) => folder.uid)));
|
|
143
|
-
}
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import type { Folder } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
6
|
+
import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The sidebar's order of a mailbox's well-known folders; anything else (`user`) follows, alphabetically. Also the order push channels are asked
|
|
10
|
+
* for (`pushChannelsFor()`), so when the server's channel cap bites it is the folders lowest in this list that go without live updates.
|
|
11
|
+
*/
|
|
12
|
+
export const FOLDER_ORDER = ["inbox", "drafts", "outbox", "sent_items", "deleted_items", "junk", "archive"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A mailbox's `calendar`/`contacts`/`tasks`/`notes` folders back their own dedicated apps (see `CalendarShell`/`ContactsShell`/`TasksShell`), not
|
|
16
|
+
* Mail - `listFolders()` returns every well-known folder for the mailbox regardless of which app owns it, so Mail's own folder tree filters down
|
|
17
|
+
* to just the mail ones itself, or those other apps' folders leak into its sidebar.
|
|
18
|
+
*/
|
|
19
|
+
export const MAIL_FOLDER_TYPES = new Set([...FOLDER_ORDER, "user"]);
|
|
20
|
+
|
|
21
|
+
/** The well-known folders the server creates lazily around a first send: what the sidebar shows a placeholder for while a message is on its way. */
|
|
22
|
+
export const SEND_FOLDER_TYPES = ["outbox", "sent_items"] as const;
|
|
23
|
+
|
|
24
|
+
export function isMailFolder(folder: { type: string }): boolean {
|
|
25
|
+
return MAIL_FOLDER_TYPES.has(folder.type);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sortKey(type: string): number {
|
|
29
|
+
const index = FOLDER_ORDER.indexOf(type);
|
|
30
|
+
return index === -1 ? FOLDER_ORDER.length : index;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Well-known folders in `FOLDER_ORDER`, then the rest by name. A new array; the input is left alone. */
|
|
34
|
+
export function sortFolders(folders: Folder[]): Folder[] {
|
|
35
|
+
return [...folders].sort((a, b) => sortKey(a.type) - sortKey(b.type) || a.name.localeCompare(b.name));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** What a sidebar row is: a folder that exists, or (`placeholder`) one that is expected to but has not arrived yet. */
|
|
39
|
+
export type FolderRow = { kind: "folder"; folder: Folder } | { kind: "placeholder"; type: string };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The rows of one mailbox's sidebar section, in order: every folder that exists, and - while `sending` messages are on their way and the
|
|
43
|
+
* server has not made the folder yet - a placeholder for each of the Outbox and Sent Items that is missing. The one place a folder that has not
|
|
44
|
+
* arrived is drawn, so it can never appear twice: the moment the real folder is in `folders` its placeholder is not built.
|
|
45
|
+
*/
|
|
46
|
+
export function folderRows(folders: Folder[], sending: number): FolderRow[] {
|
|
47
|
+
const rows: FolderRow[] = sortFolders(folders).map((folder) => ({ kind: "folder", folder }));
|
|
48
|
+
if (sending > 0) {
|
|
49
|
+
for (const type of SEND_FOLDER_TYPES) {
|
|
50
|
+
if (!folders.some((folder) => folder.type === type)) {
|
|
51
|
+
rows.push({ kind: "placeholder", type });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
rows.sort((a, b) => sortKey(rowType(a)) - sortKey(rowType(b)));
|
|
55
|
+
}
|
|
56
|
+
return rows;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rowType(row: FolderRow): string {
|
|
60
|
+
return row.kind === "folder" ? row.folder.type : row.type;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** What a folder can be changed to by an event or a refetch: everything about it except the counts, which the counts overlay owns. */
|
|
64
|
+
export type FolderChange = Partial<Pick<Folder, "name" | "type" | "parentFolderUid" | "color">> & { uid: string; mailboxUid?: string };
|
|
65
|
+
|
|
66
|
+
const CHANGEABLE = ["name", "type", "parentFolderUid", "color"] as const;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Files `folder` in the tree: added under its mailbox when unknown (a type Mail does not list is ignored), else its name, type, parent and
|
|
70
|
+
* colour brought up to date (a folder that became a type Mail does not list leaves the tree). Returns `entries` itself - the same array - when
|
|
71
|
+
* nothing changed, because everything on screen that depends on the tree starts over when it is a new one.
|
|
72
|
+
*/
|
|
73
|
+
export function upsertFolder(entries: MailboxFolders[], folder: Folder | FolderChange): MailboxFolders[] {
|
|
74
|
+
let changed = false;
|
|
75
|
+
const next = entries.map((entry) => {
|
|
76
|
+
const existing = entry.folders.find((f) => f.uid === folder.uid);
|
|
77
|
+
if (!existing) {
|
|
78
|
+
// Only a whole folder can be added: an update for one that is not known has nothing to add it with (the refetch it causes will).
|
|
79
|
+
if (entry.mailbox.uid !== folder.mailboxUid || !isFullFolder(folder) || !isMailFolder(folder)) {
|
|
80
|
+
return entry;
|
|
81
|
+
}
|
|
82
|
+
// A well-known folder is one per mailbox - the server treats the oldest as the real one - so a second of the same type (made by a client that did
|
|
83
|
+
// not know it existed) is not a second row: the older of the two is the one shown.
|
|
84
|
+
const sameType = folder.type === "user" ? undefined : entry.folders.find((f) => f.type === folder.type);
|
|
85
|
+
if (sameType) {
|
|
86
|
+
if (!isOlder(folder, sameType)) {
|
|
87
|
+
return entry;
|
|
88
|
+
}
|
|
89
|
+
changed = true;
|
|
90
|
+
return { ...entry, folders: entry.folders.map((f) => (f === sameType ? folder : f)) };
|
|
91
|
+
}
|
|
92
|
+
changed = true;
|
|
93
|
+
return { ...entry, folders: [...entry.folders, folder] };
|
|
94
|
+
}
|
|
95
|
+
const patch: Partial<Folder> = {};
|
|
96
|
+
for (const key of CHANGEABLE) {
|
|
97
|
+
if (key in folder && folder[key] !== undefined && folder[key] !== existing[key]) {
|
|
98
|
+
(patch as Record<string, unknown>)[key] = folder[key];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (Object.keys(patch).length === 0) {
|
|
102
|
+
return entry;
|
|
103
|
+
}
|
|
104
|
+
changed = true;
|
|
105
|
+
const updated = { ...existing, ...patch };
|
|
106
|
+
return { ...entry, folders: isMailFolder(updated) ? entry.folders.map((f) => (f === existing ? updated : f)) : entry.folders.filter((f) => f !== existing) };
|
|
107
|
+
});
|
|
108
|
+
return changed ? next : entries;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isFullFolder(folder: Folder | FolderChange): folder is Folder {
|
|
112
|
+
return typeof folder.name === "string" && typeof folder.type === "string";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Whether `a` was created before `b`: only when both say when. */
|
|
116
|
+
function isOlder(a: Folder, b: Folder): boolean {
|
|
117
|
+
// (A folder from the network may lack the date: `Date.parse(undefined)` is NaN, which is "cannot tell".)
|
|
118
|
+
const first = Date.parse(a.dateCreated);
|
|
119
|
+
const second = Date.parse(b.dateCreated);
|
|
120
|
+
return Number.isFinite(first) && Number.isFinite(second) && first < second;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Takes a deleted folder out of the tree. The same array when it was not in it. */
|
|
124
|
+
export function removeFolder(entries: MailboxFolders[], folderUid: string): MailboxFolders[] {
|
|
125
|
+
if (!entries.some((entry) => entry.folders.some((folder) => folder.uid === folderUid))) {
|
|
126
|
+
return entries;
|
|
127
|
+
}
|
|
128
|
+
return entries.map((entry) => (entry.folders.some((folder) => folder.uid === folderUid) ? { ...entry, folders: entry.folders.filter((folder) => folder.uid !== folderUid) } : entry));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Brings one mailbox's part of the tree up to date with a listing the server just gave (`listFolders()`): folders it has that the tree lacks
|
|
133
|
+
* are added, changed names and types are taken. Nothing is removed - a listing that began before a folder was created here would take it out
|
|
134
|
+
* again, and deletions arrive as events - and the counts are not touched (see `useFolderCounts()`). The same array when there is nothing to do.
|
|
135
|
+
*/
|
|
136
|
+
export function reconcileFolders(entries: MailboxFolders[], listed: Folder[]): MailboxFolders[] {
|
|
137
|
+
return listed.reduce((tree, folder) => upsertFolder(tree, folder), entries);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Every folder uid in the tree. */
|
|
141
|
+
export function knownFolderUids(entries: MailboxFolders[]): Set<string> {
|
|
142
|
+
return new Set(entries.flatMap((entry) => entry.folders.map((folder) => folder.uid)));
|
|
143
|
+
}
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
|
|
6
|
-
/** Page size the list pages (Contacts, Tasks) request per call - restapi caps `limit` at 500. */
|
|
7
|
-
export const LIST_PAGE_SIZE = 500;
|
|
8
|
-
|
|
9
|
-
/** Safety cap on pages fetched - stops a server that ignores `page` (and keeps returning a full first
|
|
10
|
-
* page) from looping forever. 40 pages x 500 = 20,000 items. */
|
|
11
|
-
export const MAX_LIST_PAGES = 40;
|
|
12
|
-
|
|
13
|
-
export interface ListAllPagesResult<T> {
|
|
14
|
-
items: T[];
|
|
15
|
-
/** `true` when fetching stopped at `maxPages` with the last page still full - there may be more items
|
|
16
|
-
* the caller isn't showing, so it should say so rather than presenting `items` as the whole list. */
|
|
17
|
-
truncated: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Fetches every page of a zero-based `limit`/`page` list endpoint, stopping at the first short page
|
|
22
|
-
* (fewer than `pageSize` items) - so a folder with more items than one page holds is shown in full
|
|
23
|
-
* rather than silently cut off at the first page. Stops at `maxPages` regardless, reporting `truncated`.
|
|
24
|
-
*/
|
|
25
|
-
export async function listAllPages<T>(
|
|
26
|
-
fetchPage: (page: number) => Promise<T[]>,
|
|
27
|
-
pageSize: number = LIST_PAGE_SIZE,
|
|
28
|
-
maxPages: number = MAX_LIST_PAGES,
|
|
29
|
-
): Promise<ListAllPagesResult<T>> {
|
|
30
|
-
const items: T[] = [];
|
|
31
|
-
for (let page = 0; page < maxPages; page++) {
|
|
32
|
-
const batch = await fetchPage(page);
|
|
33
|
-
items.push(...batch);
|
|
34
|
-
if (batch.length < pageSize) {
|
|
35
|
-
return { items, truncated: false };
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return { items, truncated: true };
|
|
39
|
-
}
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
|
|
6
|
+
/** Page size the list pages (Contacts, Tasks) request per call - restapi caps `limit` at 500. */
|
|
7
|
+
export const LIST_PAGE_SIZE = 500;
|
|
8
|
+
|
|
9
|
+
/** Safety cap on pages fetched - stops a server that ignores `page` (and keeps returning a full first
|
|
10
|
+
* page) from looping forever. 40 pages x 500 = 20,000 items. */
|
|
11
|
+
export const MAX_LIST_PAGES = 40;
|
|
12
|
+
|
|
13
|
+
export interface ListAllPagesResult<T> {
|
|
14
|
+
items: T[];
|
|
15
|
+
/** `true` when fetching stopped at `maxPages` with the last page still full - there may be more items
|
|
16
|
+
* the caller isn't showing, so it should say so rather than presenting `items` as the whole list. */
|
|
17
|
+
truncated: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fetches every page of a zero-based `limit`/`page` list endpoint, stopping at the first short page
|
|
22
|
+
* (fewer than `pageSize` items) - so a folder with more items than one page holds is shown in full
|
|
23
|
+
* rather than silently cut off at the first page. Stops at `maxPages` regardless, reporting `truncated`.
|
|
24
|
+
*/
|
|
25
|
+
export async function listAllPages<T>(
|
|
26
|
+
fetchPage: (page: number) => Promise<T[]>,
|
|
27
|
+
pageSize: number = LIST_PAGE_SIZE,
|
|
28
|
+
maxPages: number = MAX_LIST_PAGES,
|
|
29
|
+
): Promise<ListAllPagesResult<T>> {
|
|
30
|
+
const items: T[] = [];
|
|
31
|
+
for (let page = 0; page < maxPages; page++) {
|
|
32
|
+
const batch = await fetchPage(page);
|
|
33
|
+
items.push(...batch);
|
|
34
|
+
if (batch.length < pageSize) {
|
|
35
|
+
return { items, truncated: false };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { items, truncated: true };
|
|
39
|
+
}
|
|
@@ -6,17 +6,14 @@ import type { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailAp
|
|
|
6
6
|
import { formatMailAddress, splitMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* What decides whether a new message is announced, what the announcement says, and the
|
|
10
|
-
*
|
|
11
|
-
* notifications
|
|
9
|
+
* What decides whether a new message is announced, what the announcement says, and the setting behind it (per browser, in
|
|
10
|
+
* `localStorage`): what the user told the "turn on desktop notifications" offer. Whether pop-ups are on at all is the
|
|
11
|
+
* notifications switch (`notifications/preferences.ts`).
|
|
12
12
|
*
|
|
13
13
|
* The announcement is built only from what the push event carries - the whole `Message`, including `bodyPreview` - and only
|
|
14
14
|
* ever as text: React escapes it on screen and the Notifications API takes plain strings, so nothing here is HTML.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
/** `localStorage` key of the pop-ups switch. Absent means on; `"off"` means the user turned them off. */
|
|
18
|
-
export const NEW_MAIL_POPUPS_KEY = "rapidmx-new-mail-popups";
|
|
19
|
-
|
|
20
17
|
/** `localStorage` key of the answer to the desktop-notifications offer: `"later"` (not now) or `"asked"` (they were asked). */
|
|
21
18
|
export const DESKTOP_OFFER_KEY = "rapidmx-desktop-notifications-offer";
|
|
22
19
|
|
|
@@ -49,15 +46,6 @@ function writeStorage(key: string, value: string | null): void {
|
|
|
49
46
|
}
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
/** Whether new-mail pop-ups are on (the default). */
|
|
53
|
-
export function getNewMailPopupsEnabled(): boolean {
|
|
54
|
-
return readStorage(NEW_MAIL_POPUPS_KEY) !== "off";
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function setNewMailPopupsEnabled(enabled: boolean): void {
|
|
58
|
-
writeStorage(NEW_MAIL_POPUPS_KEY, enabled ? null : "off");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
49
|
/** Whether the "turn on desktop notifications" offer has been put away with "Not now" (or answered). */
|
|
62
50
|
export function getDesktopOfferDismissed(): boolean {
|
|
63
51
|
return readStorage(DESKTOP_OFFER_KEY) !== null;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useSyncExternalStore } from "react";
|
|
6
|
+
import type { Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
7
|
+
import type { MailAddressLike } from "@rapidmx/react-shared/mail/mailAddress.js";
|
|
8
|
+
import type { NotificationAction } from "../../notifications/store.js";
|
|
9
|
+
import type { SendRequest } from "./sendJob.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A reply (or forward) this tab has sent, as the open conversation shows it until the server's own copy is in the thread.
|
|
13
|
+
*
|
|
14
|
+
* This is a *view* of the background send that is already there, not a second state machine: `startSend()` (`sendJob.ts`) opens an entry, and the
|
|
15
|
+
* places that already report how a send went close it - the client-side failure (`notifyFailure()`), the server's own outcome (`handleSendEvent()`
|
|
16
|
+
* in `sendOutcomes.ts`: `send-succeeded`, `send-failed`) and a synchronous send. The Outbox indicator, the pop-ups and this entry therefore agree,
|
|
17
|
+
* because they are all told by the same code at the same moment.
|
|
18
|
+
*
|
|
19
|
+
* Why the message has to be shown from here for a while: a message the server accepted is in Outbox, which has no `conversationId` yet - it is
|
|
20
|
+
* resolved when the message is relayed and filed in Sent Items (`ScheduledSendJob`), keeping the same `uid`. Until then no fetch of the
|
|
21
|
+
* conversation can return it, so the thread pane draws it from what was composed.
|
|
22
|
+
*
|
|
23
|
+
* `sending` covers everything up to the server's answer (assembling, the request, the queue in Outbox); `failed` is a send that did not go and is
|
|
24
|
+
* the sender's to resolve (`actions` are the very ones the pop-up offers: Retry, Open draft ...); `sent` is a message the server has relayed and
|
|
25
|
+
* that is waiting for the thread to be read again to find its Sent Items copy.
|
|
26
|
+
*/
|
|
27
|
+
export type OutgoingState = "sending" | "failed" | "sent";
|
|
28
|
+
|
|
29
|
+
export interface OutgoingReply {
|
|
30
|
+
/** The draft's uid - which is also the uid of the message in Outbox and of its Sent Items copy, and so what the real message replaces this by. */
|
|
31
|
+
uid: string;
|
|
32
|
+
mailboxUid: string;
|
|
33
|
+
/** What the message replies to (`Message.inReplyTo` / `references`): how the pane knows which conversation it belongs to. */
|
|
34
|
+
inReplyTo?: string;
|
|
35
|
+
references: string[];
|
|
36
|
+
sender: MailAddressLike;
|
|
37
|
+
to: MailAddressLike[];
|
|
38
|
+
cc: MailAddressLike[];
|
|
39
|
+
bcc: MailAddressLike[];
|
|
40
|
+
subject: string;
|
|
41
|
+
/** The composed body, as the editor wrote it. */
|
|
42
|
+
html: string;
|
|
43
|
+
startedAt: number;
|
|
44
|
+
state: OutgoingState;
|
|
45
|
+
/** `failed`: why. */
|
|
46
|
+
failure?: string;
|
|
47
|
+
/** `failed`: what the sender can do about it. */
|
|
48
|
+
actions: NotificationAction[];
|
|
49
|
+
/** `sent`: how many times the thread was read without finding the message - see `settleOutgoing()`. */
|
|
50
|
+
misses: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The most entries kept; the oldest go first. Every entry is a message this tab sent, so this is generous. */
|
|
54
|
+
const MAX_TRACKED = 25;
|
|
55
|
+
/** A sent message that a read of its conversation has not found this many times is not part of that conversation (the server filed it elsewhere). */
|
|
56
|
+
export const MAX_SETTLE_MISSES = 2;
|
|
57
|
+
|
|
58
|
+
const NO_REPLIES: readonly OutgoingReply[] = [];
|
|
59
|
+
let replies: OutgoingReply[] = [];
|
|
60
|
+
let snapshot: readonly OutgoingReply[] = replies;
|
|
61
|
+
const listeners = new Set<() => void>();
|
|
62
|
+
|
|
63
|
+
function commit(next: OutgoingReply[]): void {
|
|
64
|
+
replies = next;
|
|
65
|
+
snapshot = next;
|
|
66
|
+
for (const listener of [...listeners]) {
|
|
67
|
+
listener();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function update(uid: string, patch: Partial<OutgoingReply>): void {
|
|
72
|
+
if (replies.some((reply) => reply.uid === uid)) {
|
|
73
|
+
commit(replies.map((reply) => (reply.uid === uid ? { ...reply, ...patch } : reply)));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Starts showing `request`'s message in the conversation it replies to - or does nothing for a message that replies to nothing (a new message
|
|
79
|
+
* starts a conversation of its own, it is never part of an open one) and for a send-later message (it is not sent yet). Called again for the same
|
|
80
|
+
* message - a Retry - it starts over as `sending`.
|
|
81
|
+
*
|
|
82
|
+
* The thread is `request.threading` (the compose session's), else what the draft itself records: a draft opened again from "Open draft" is a
|
|
83
|
+
* session with no threading of its own, but the server kept it on the message.
|
|
84
|
+
*/
|
|
85
|
+
export function trackOutgoing(request: SendRequest): void {
|
|
86
|
+
const source = request.threading ?? request.draft;
|
|
87
|
+
const references = source.references ?? [];
|
|
88
|
+
if (request.scheduledSendTime || (!source.inReplyTo && references.length === 0)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const { mailbox } = request;
|
|
92
|
+
const reply: OutgoingReply = {
|
|
93
|
+
uid: request.draft.uid,
|
|
94
|
+
mailboxUid: request.mailboxUid,
|
|
95
|
+
inReplyTo: source.inReplyTo,
|
|
96
|
+
references,
|
|
97
|
+
// The mailbox is only unknown when the window's own load of it had not succeeded: "Me" is what a sender is called then.
|
|
98
|
+
sender: mailbox ? { address: mailbox.primarySmtpAddress, displayName: mailbox.displayName } : { address: "Me" },
|
|
99
|
+
to: request.to,
|
|
100
|
+
cc: request.cc,
|
|
101
|
+
bcc: request.bcc,
|
|
102
|
+
subject: request.subject,
|
|
103
|
+
html: request.html,
|
|
104
|
+
startedAt: Date.now(),
|
|
105
|
+
state: "sending",
|
|
106
|
+
actions: [],
|
|
107
|
+
misses: 0,
|
|
108
|
+
};
|
|
109
|
+
commit([...replies.filter((existing) => existing.uid !== reply.uid), reply].slice(-MAX_TRACKED));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The send did not go: `failure` is why, `actions` what can be done (Retry, Open draft ...). */
|
|
113
|
+
export function failOutgoing(uid: string, failure: string, actions: NotificationAction[]): void {
|
|
114
|
+
update(uid, { state: "failed", failure, actions });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A failed send is being tried again. */
|
|
118
|
+
export function markOutgoingSending(uid: string): void {
|
|
119
|
+
update(uid, { state: "sending", failure: undefined, actions: [] });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The server has relayed the message: its Sent Items copy is what the conversation shows next. */
|
|
123
|
+
export function markOutgoingSent(uid: string): void {
|
|
124
|
+
update(uid, { state: "sent" });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Stops showing a message - it went back to Drafts to be edited. */
|
|
128
|
+
export function forgetOutgoing(uid: string): void {
|
|
129
|
+
if (replies.some((reply) => reply.uid === uid)) {
|
|
130
|
+
commit(replies.filter((reply) => reply.uid !== uid));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Whether `reply` continues the conversation `messages` are: it answers one of them, or its `References` chain runs through one. */
|
|
135
|
+
export function belongsToThread(reply: OutgoingReply, messages: readonly Pick<Message, "messageId">[]): boolean {
|
|
136
|
+
return messages.some((message) => !!message.messageId && (message.messageId === reply.inReplyTo || reply.references.includes(message.messageId)));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The uids of the messages sent from here whose real copy is in `messages`, the thread as it was just read. */
|
|
140
|
+
export function adoptedOutgoing(mailboxUid: string, messages: readonly Message[]): string[] {
|
|
141
|
+
return replies.filter((reply) => reply.mailboxUid === mailboxUid && messages.some((message) => message.uid === reply.uid)).map((reply) => reply.uid);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Called with a mailbox's conversation as it was just read, `messages` being the whole thread: forgets the messages whose real copy is now in it
|
|
146
|
+
* (`adoptedOutgoing()`), and gives up on a *sent* message that belongs to this thread but that the read did not find `MAX_SETTLE_MISSES` times in a
|
|
147
|
+
* row - the server filed it in a conversation of its own, and it is not to be shown here. A message of another conversation is left alone.
|
|
148
|
+
*
|
|
149
|
+
* A pane draws the messages it has read *before* it calls this, so that there is never a moment with neither the pending card nor the real one.
|
|
150
|
+
*/
|
|
151
|
+
export function settleOutgoing(mailboxUid: string, messages: readonly Message[]): void {
|
|
152
|
+
const adopted = new Set(adoptedOutgoing(mailboxUid, messages));
|
|
153
|
+
const next: OutgoingReply[] = [];
|
|
154
|
+
let changed = adopted.size > 0;
|
|
155
|
+
for (const reply of replies) {
|
|
156
|
+
if (adopted.has(reply.uid)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (reply.mailboxUid === mailboxUid && reply.state === "sent" && belongsToThread(reply, messages)) {
|
|
160
|
+
changed = true;
|
|
161
|
+
if (reply.misses + 1 < MAX_SETTLE_MISSES) {
|
|
162
|
+
next.push({ ...reply, misses: reply.misses + 1 });
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
next.push(reply);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (changed) {
|
|
169
|
+
commit(next);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getOutgoingReplies(): readonly OutgoingReply[] {
|
|
174
|
+
return snapshot;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function subscribe(listener: () => void): () => void {
|
|
178
|
+
listeners.add(listener);
|
|
179
|
+
return () => {
|
|
180
|
+
listeners.delete(listener);
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The messages this tab has sent that a conversation may still need to show, oldest first. */
|
|
185
|
+
export function useOutgoingReplies(): readonly OutgoingReply[] {
|
|
186
|
+
return useSyncExternalStore(subscribe, getOutgoingReplies, () => NO_REPLIES);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Forgets everything - for a test. */
|
|
190
|
+
export function resetOutgoingReplies(): void {
|
|
191
|
+
commit([]);
|
|
192
|
+
}
|