@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { validDate } from "./inviteFormat.js";
|
|
3
|
+
/** How tall one hour is drawn, in px. */
|
|
4
|
+
const HOUR_HEIGHT = 36;
|
|
5
|
+
/** The room the hour labels take at the left, as a CSS length. */
|
|
6
|
+
const LABEL_WIDTH = "3.25rem";
|
|
7
|
+
/** The hours of context shown before the invitation and after it, and the least the day view is tall (in hours). */
|
|
8
|
+
const CONTEXT_HOURS = 2;
|
|
9
|
+
const MIN_HOURS = 5;
|
|
10
|
+
const MINUTE_MS = 60000;
|
|
11
|
+
/**
|
|
12
|
+
* Lays overlapping blocks side by side: blocks that overlap (directly or through a chain of others) form a cluster, and each block takes the first
|
|
13
|
+
* column of the cluster that is free where it starts. `blocks` must be sorted by start; the result is in the same order.
|
|
14
|
+
*/
|
|
15
|
+
export function placeBlocks(blocks) {
|
|
16
|
+
const placed = [];
|
|
17
|
+
let cluster = [];
|
|
18
|
+
let columnEnds = [];
|
|
19
|
+
let clusterEnd = -Infinity;
|
|
20
|
+
const close = () => {
|
|
21
|
+
for (const member of cluster) {
|
|
22
|
+
placed[member.index] = { column: member.column, columns: columnEnds.length };
|
|
23
|
+
}
|
|
24
|
+
cluster = [];
|
|
25
|
+
columnEnds = [];
|
|
26
|
+
};
|
|
27
|
+
blocks.forEach((block, index) => {
|
|
28
|
+
if (block.start >= clusterEnd) {
|
|
29
|
+
close();
|
|
30
|
+
}
|
|
31
|
+
let column = columnEnds.findIndex((end) => end <= block.start);
|
|
32
|
+
if (column === -1) {
|
|
33
|
+
column = columnEnds.length;
|
|
34
|
+
}
|
|
35
|
+
columnEnds[column] = block.end;
|
|
36
|
+
cluster.push({ index, column });
|
|
37
|
+
clusterEnd = Math.max(clusterEnd, block.end);
|
|
38
|
+
});
|
|
39
|
+
close();
|
|
40
|
+
return placed;
|
|
41
|
+
}
|
|
42
|
+
/** The hours the day view spans: the invitation's own, with `CONTEXT_HOURS` either side (at least `MIN_HOURS`), kept inside the invitation's day. */
|
|
43
|
+
function hourWindow(start, end) {
|
|
44
|
+
const sameDay = start.toDateString() === end.toDateString();
|
|
45
|
+
const endHour = sameDay ? Math.ceil(end.getHours() + end.getMinutes() / 60) : 24;
|
|
46
|
+
let from = Math.max(0, start.getHours() - CONTEXT_HOURS);
|
|
47
|
+
let to = Math.min(24, endHour + CONTEXT_HOURS);
|
|
48
|
+
if (to - from < MIN_HOURS) {
|
|
49
|
+
to = Math.min(24, from + MIN_HOURS);
|
|
50
|
+
from = Math.max(0, to - MIN_HOURS);
|
|
51
|
+
}
|
|
52
|
+
return { from, to };
|
|
53
|
+
}
|
|
54
|
+
const BLOCK_CLASS = {
|
|
55
|
+
invite: "bg-primary/20 border-primary text-text font-semibold",
|
|
56
|
+
conflict: "bg-danger-bg border-danger text-danger",
|
|
57
|
+
busy: "bg-surface border-border text-text",
|
|
58
|
+
free: "bg-transparent border-border text-text-muted",
|
|
59
|
+
};
|
|
60
|
+
const CLOCK = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit" });
|
|
61
|
+
const HOUR = new Intl.DateTimeFormat(undefined, { hour: "numeric" });
|
|
62
|
+
/**
|
|
63
|
+
* A small day agenda around an invitation, as Outlook draws it in its RSVP card: hour rows for the invitation's day (from a couple of hours before it
|
|
64
|
+
* to a couple after), the invitation itself as a highlighted block, and the reader's other events as blocks - the ones that overlap it marked in red
|
|
65
|
+
* (and named as conflicts to a screen reader). Overlapping blocks stand side by side. All-day events are listed above the hours rather than drawn.
|
|
66
|
+
* Draws nothing when the invitation has no usable time.
|
|
67
|
+
*/
|
|
68
|
+
export default function InviteTimeline({ invite }) {
|
|
69
|
+
const start = validDate(invite.startDate);
|
|
70
|
+
const end = validDate(invite.endDate);
|
|
71
|
+
if (!start || !end || end <= start) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const { from, to } = hourWindow(start, end);
|
|
75
|
+
const windowStart = new Date(start.getFullYear(), start.getMonth(), start.getDate(), from).getTime();
|
|
76
|
+
const windowEnd = new Date(start.getFullYear(), start.getMonth(), start.getDate(), to).getTime();
|
|
77
|
+
const conflictUids = new Set(invite.conflicts.map((entry) => entry.uid));
|
|
78
|
+
// A conflict the schedule doesn't list is still drawn.
|
|
79
|
+
const entries = [...invite.schedule, ...invite.conflicts.filter((c) => !invite.schedule.some((s) => s.uid === c.uid))];
|
|
80
|
+
const allDay = entries.filter((entry) => entry.allDay);
|
|
81
|
+
const blocks = [{ key: "invite", title: invite.summary?.trim() || "(no title)", start: start.getTime(), end: end.getTime(), kind: "invite", tentative: false }];
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const entryStart = validDate(entry.startDate)?.getTime();
|
|
84
|
+
const entryEnd = validDate(entry.endDate)?.getTime();
|
|
85
|
+
if (entry.allDay || entryStart === undefined || entryEnd === undefined) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
blocks.push({
|
|
89
|
+
key: entry.uid,
|
|
90
|
+
title: entry.title || "(no title)",
|
|
91
|
+
start: entryStart,
|
|
92
|
+
end: entryEnd,
|
|
93
|
+
kind: conflictUids.has(entry.uid) ? "conflict" : entry.busy ? "busy" : "free",
|
|
94
|
+
tentative: entry.tentative,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Clipped to the hours shown; a block entirely outside them isn't drawn.
|
|
98
|
+
const visible = blocks
|
|
99
|
+
.map((block) => ({ ...block, start: Math.max(block.start, windowStart), end: Math.min(block.end, windowEnd), fullStart: block.start, fullEnd: block.end }))
|
|
100
|
+
.filter((block) => block.end > block.start)
|
|
101
|
+
.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
102
|
+
const placed = placeBlocks(visible);
|
|
103
|
+
const hours = Array.from({ length: to - from }, (_, i) => from + i);
|
|
104
|
+
return (_jsxs("div", { "aria-label": "Your schedule around this meeting", role: "group", className: "text-xs", children: [allDay.length > 0 && (_jsx("ul", { "aria-label": "All-day events", className: "mb-1 flex flex-col gap-0.5", children: allDay.map((entry) => (_jsxs("li", { className: "px-2 py-0.5 rounded-sm border border-border bg-surface text-text-muted truncate", children: ["All day: ", entry.title || "(no title)"] }, entry.uid))) })), _jsxs("div", { className: "relative border-t border-border", style: { height: hours.length * HOUR_HEIGHT }, children: [hours.map((hour) => (_jsx("div", { "aria-hidden": "true", className: "absolute inset-x-0 border-b border-border text-text-muted", style: { top: (hour - from) * HOUR_HEIGHT, height: HOUR_HEIGHT }, children: _jsx("span", { className: "absolute left-0 top-0 pr-1 leading-none pt-0.5", style: { width: LABEL_WIDTH }, children: HOUR.format(new Date(2000, 0, 1, hour)) }) }, hour))), _jsx("ul", { "aria-label": "Events", children: visible.map((block, index) => {
|
|
105
|
+
const { column, columns } = placed[index];
|
|
106
|
+
const top = ((block.start - windowStart) / MINUTE_MS / 60) * HOUR_HEIGHT;
|
|
107
|
+
const height = Math.max(18, ((block.end - block.start) / MINUTE_MS / 60) * HOUR_HEIGHT - 1);
|
|
108
|
+
const range = `${CLOCK.format(block.fullStart)} to ${CLOCK.format(block.fullEnd)}`;
|
|
109
|
+
return (_jsxs("li", { title: `${block.title}, ${range}`, "data-kind": block.kind, className: [
|
|
110
|
+
"absolute overflow-hidden rounded-sm border px-1.5 py-0.5 leading-tight",
|
|
111
|
+
BLOCK_CLASS[block.kind],
|
|
112
|
+
block.tentative ? "border-dashed" : "",
|
|
113
|
+
].join(" "), style: {
|
|
114
|
+
top,
|
|
115
|
+
height,
|
|
116
|
+
left: `calc(${LABEL_WIDTH} + (100% - ${LABEL_WIDTH}) * ${column} / ${columns})`,
|
|
117
|
+
width: `calc((100% - ${LABEL_WIDTH}) / ${columns} - 2px)`,
|
|
118
|
+
}, children: [_jsx("span", { className: "block truncate", children: block.title }), _jsxs("span", { className: "sr-only", children: [", ", range, block.kind === "invite" ? ", this meeting" : "", block.kind === "conflict" ? ", conflicts with this meeting" : "", block.tentative ? ", tentative" : ""] })] }, block.key));
|
|
119
|
+
}) })] })] }));
|
|
120
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { MessageInvite, ProposedTime } from "@rapidmx/react-shared/calendar/inviteApi.js";
|
|
3
|
+
export interface ProposeTimeFormProps {
|
|
4
|
+
/** The invitation being answered: its own time (in the reader's zone) is what the fields start out as. */
|
|
5
|
+
invite: Pick<MessageInvite, "startDate" | "endDate">;
|
|
6
|
+
/** A proposal is on its way: the form is off. */
|
|
7
|
+
busy?: boolean;
|
|
8
|
+
/** Called with the proposed instants once the fields are valid. The caller reports a failure and closes the form on success. */
|
|
9
|
+
onSubmit: (proposal: ProposedTime) => void;
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The small form behind "Propose new time": a date and a start and end time - in the reader's own time zone, prefilled with the invitation's own
|
|
14
|
+
* time - and an optional note to the organizer. It validates what it can (all three fields, and an end after the start) before anything is sent.
|
|
15
|
+
* The end is on the start's day: a proposal spans one day, as Outlook's does from a list.
|
|
16
|
+
*/
|
|
17
|
+
export default function ProposeTimeForm({ invite, busy, onSubmit, onCancel }: ProposeTimeFormProps): React.JSX.Element;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useId, useState } from "react";
|
|
7
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
8
|
+
import { isoDay, validDate } from "./inviteFormat.js";
|
|
9
|
+
const INPUT_CLASS = "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface text-text";
|
|
10
|
+
/** `HH:MM` of an instant in the reader's zone, the value of a time input. */
|
|
11
|
+
function clock(date) {
|
|
12
|
+
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The small form behind "Propose new time": a date and a start and end time - in the reader's own time zone, prefilled with the invitation's own
|
|
16
|
+
* time - and an optional note to the organizer. It validates what it can (all three fields, and an end after the start) before anything is sent.
|
|
17
|
+
* The end is on the start's day: a proposal spans one day, as Outlook's does from a list.
|
|
18
|
+
*/
|
|
19
|
+
export default function ProposeTimeForm({ invite, busy = false, onSubmit, onCancel }) {
|
|
20
|
+
const start = validDate(invite.startDate);
|
|
21
|
+
const end = validDate(invite.endDate);
|
|
22
|
+
const [date, setDate] = useState(start ? isoDay(start) : "");
|
|
23
|
+
const [startTime, setStartTime] = useState(start ? clock(start) : "");
|
|
24
|
+
const [endTime, setEndTime] = useState(end ? clock(end) : "");
|
|
25
|
+
const [comment, setComment] = useState("");
|
|
26
|
+
const [error, setError] = useState(null);
|
|
27
|
+
const id = useId();
|
|
28
|
+
function submit(event) {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
if (!date || !startTime || !endTime) {
|
|
31
|
+
setError("Choose a date and a start and end time.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const proposedStart = new Date(`${date}T${startTime}:00`);
|
|
35
|
+
const proposedEnd = new Date(`${date}T${endTime}:00`);
|
|
36
|
+
if (proposedEnd <= proposedStart) {
|
|
37
|
+
setError("The end time must be after the start time.");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setError(null);
|
|
41
|
+
const note = comment.trim();
|
|
42
|
+
onSubmit({ startDate: proposedStart.toISOString(), endDate: proposedEnd.toISOString(), ...(note ? { comment: note } : {}) });
|
|
43
|
+
}
|
|
44
|
+
return (_jsxs("form", { "aria-label": "Propose a new time", onSubmit: submit, noValidate: true, className: "flex flex-col gap-2 text-sm", children: [_jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsxs("div", { className: "flex-1 min-w-[9rem]", children: [_jsx("label", { htmlFor: `${id}-date`, className: "block text-xs text-text-muted mb-0.5", children: "Date" }), _jsx("input", { id: `${id}-date`, type: "date", value: date, disabled: busy, onChange: (e) => setDate(e.target.value), className: INPUT_CLASS })] }), _jsxs("div", { className: "flex-1 min-w-[6rem]", children: [_jsx("label", { htmlFor: `${id}-start`, className: "block text-xs text-text-muted mb-0.5", children: "Start" }), _jsx("input", { id: `${id}-start`, type: "time", value: startTime, disabled: busy, onChange: (e) => setStartTime(e.target.value), className: INPUT_CLASS })] }), _jsxs("div", { className: "flex-1 min-w-[6rem]", children: [_jsx("label", { htmlFor: `${id}-end`, className: "block text-xs text-text-muted mb-0.5", children: "End" }), _jsx("input", { id: `${id}-end`, type: "time", value: endTime, disabled: busy, onChange: (e) => setEndTime(e.target.value), className: INPUT_CLASS })] })] }), _jsxs("div", { children: [_jsx("label", { htmlFor: `${id}-comment`, className: "block text-xs text-text-muted mb-0.5", children: "Comment (optional)" }), _jsx("textarea", { id: `${id}-comment`, rows: 2, value: comment, disabled: busy, onChange: (e) => setComment(e.target.value), className: `${INPUT_CLASS} resize-y` })] }), error && (_jsx("p", { role: "alert", className: "text-danger", children: error })), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { type: "submit", className: "!w-auto", loading: busy, disabled: busy, "aria-busy": busy || undefined, children: "Send proposal" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: onCancel, children: "Cancel" })] })] }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Attachment } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
2
|
+
import type { InviteResponse, MessageInvite } from "@rapidmx/react-shared/calendar/inviteApi.js";
|
|
3
|
+
/** Whether an attachment is a calendar file - what tells the pane a message may carry an invitation worth asking the server about. By type
|
|
4
|
+
* or by name, since mail clients disagree about which one they set (`text/calendar`, `application/ics`, or a bare `.ics` as octet-stream). */
|
|
5
|
+
export declare function isCalendarAttachment(attachment: Pick<Attachment, "filename" | "mimeType">): boolean;
|
|
6
|
+
/** `zone` if the browser knows it, else UTC - an all-day date is a calendar day, and needs a fixed zone to stay the same day for every reader. */
|
|
7
|
+
export declare function allDayZone(zone: string | undefined): string;
|
|
8
|
+
/** The instant an ISO string names, or `undefined` when it is missing or unparseable. */
|
|
9
|
+
export declare function validDate(iso: string | undefined): Date | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* When the meeting is, in the reader's own locale and time zone: one date and a time range for a same-day event, both ends in full for
|
|
12
|
+
* one that spans days, and dates only for an all-day event (drawn in the organizer's zone, since a calendar day is not an instant). `undefined`
|
|
13
|
+
* when the invitation names no usable start. A missing or unusable end shows the start alone.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatInviteWhen(invite: Pick<MessageInvite, "startDate" | "endDate" | "allDay" | "timezone">): string | undefined;
|
|
16
|
+
/** The meeting's start the way a list row writes it - `Thu 9/24/2026 1:00 PM` (dates only for an all-day event), in the reader's zone. */
|
|
17
|
+
export declare function formatRowStart(invite: Pick<MessageInvite, "startDate" | "allDay" | "timezone">): string | undefined;
|
|
18
|
+
/** `YYYY-MM-DD` of an instant in the reader's zone, or in `timeZone` when given (`en-CA` writes a date that way). */
|
|
19
|
+
export declare function isoDay(date: Date, timeZone?: string): string;
|
|
20
|
+
/** Where "Open in Calendar" goes: the day view of the day the meeting starts (the `date`/`view` the calendar page reads), or the calendar itself
|
|
21
|
+
* when the invitation names no usable start. An all-day event's day is the organizer's; any other is the reader's. */
|
|
22
|
+
export declare function calendarHref(invite: Pick<MessageInvite, "startDate" | "allDay" | "timezone">): string;
|
|
23
|
+
/** The reader's answer as a list row says it. */
|
|
24
|
+
export declare const ANSWER_LABEL: Record<InviteResponse, string>;
|
|
25
|
+
/** The titles of the events an invitation conflicts with, as "A, B and 2 more". */
|
|
26
|
+
export declare function conflictSummary(conflicts: {
|
|
27
|
+
title: string;
|
|
28
|
+
}[]): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { APP_HREFS } from "../../../navigation/appHrefs.js";
|
|
2
|
+
/** Whether an attachment is a calendar file - what tells the pane a message may carry an invitation worth asking the server about. By type
|
|
3
|
+
* or by name, since mail clients disagree about which one they set (`text/calendar`, `application/ics`, or a bare `.ics` as octet-stream). */
|
|
4
|
+
export function isCalendarAttachment(attachment) {
|
|
5
|
+
const type = (attachment.mimeType ?? "").split(";")[0].trim().toLowerCase();
|
|
6
|
+
return type === "text/calendar" || type === "application/ics" || (attachment.filename ?? "").trim().toLowerCase().endsWith(".ics");
|
|
7
|
+
}
|
|
8
|
+
const DAY_FORMAT = { weekday: "short", year: "numeric", month: "short", day: "numeric" };
|
|
9
|
+
const CLOCK_FORMAT = { hour: "numeric", minute: "2-digit" };
|
|
10
|
+
/** `zone` if the browser knows it, else UTC - an all-day date is a calendar day, and needs a fixed zone to stay the same day for every reader. */
|
|
11
|
+
export function allDayZone(zone) {
|
|
12
|
+
try {
|
|
13
|
+
new Intl.DateTimeFormat(undefined, { timeZone: zone });
|
|
14
|
+
return zone ?? "UTC";
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return "UTC";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The instant an ISO string names, or `undefined` when it is missing or unparseable. */
|
|
21
|
+
export function validDate(iso) {
|
|
22
|
+
const date = iso ? new Date(iso) : undefined;
|
|
23
|
+
return date && !isNaN(date.getTime()) ? date : undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* When the meeting is, in the reader's own locale and time zone: one date and a time range for a same-day event, both ends in full for
|
|
27
|
+
* one that spans days, and dates only for an all-day event (drawn in the organizer's zone, since a calendar day is not an instant). `undefined`
|
|
28
|
+
* when the invitation names no usable start. A missing or unusable end shows the start alone.
|
|
29
|
+
*/
|
|
30
|
+
export function formatInviteWhen(invite) {
|
|
31
|
+
const start = validDate(invite.startDate);
|
|
32
|
+
if (!start) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const parsedEnd = validDate(invite.endDate);
|
|
36
|
+
const end = parsedEnd && parsedEnd > start ? parsedEnd : undefined;
|
|
37
|
+
if (invite.allDay) {
|
|
38
|
+
const day = new Intl.DateTimeFormat(undefined, { ...DAY_FORMAT, timeZone: allDayZone(invite.timezone) });
|
|
39
|
+
const first = day.format(start);
|
|
40
|
+
// An all-day event's end is the midnight after its last day (iCalendar's DTEND is exclusive), so the last day is a moment before it.
|
|
41
|
+
const last = end ? day.format(new Date(end.getTime() - 1)) : first;
|
|
42
|
+
return last === first ? first : `${first} - ${last}`;
|
|
43
|
+
}
|
|
44
|
+
const date = new Intl.DateTimeFormat(undefined, DAY_FORMAT);
|
|
45
|
+
const time = new Intl.DateTimeFormat(undefined, CLOCK_FORMAT);
|
|
46
|
+
// The zone is named once, after the last time shown: the times are the reader's own, not the organizer's.
|
|
47
|
+
const timeAndZone = new Intl.DateTimeFormat(undefined, { ...CLOCK_FORMAT, timeZoneName: "short" });
|
|
48
|
+
const startDay = date.format(start);
|
|
49
|
+
if (!end) {
|
|
50
|
+
return `${startDay}, ${timeAndZone.format(start)}`;
|
|
51
|
+
}
|
|
52
|
+
return startDay === date.format(end)
|
|
53
|
+
? `${startDay}, ${time.format(start)} - ${timeAndZone.format(end)}`
|
|
54
|
+
: `${startDay}, ${time.format(start)} - ${date.format(end)}, ${timeAndZone.format(end)}`;
|
|
55
|
+
}
|
|
56
|
+
/** The meeting's start the way a list row writes it - `Thu 9/24/2026 1:00 PM` (dates only for an all-day event), in the reader's zone. */
|
|
57
|
+
export function formatRowStart(invite) {
|
|
58
|
+
const start = validDate(invite.startDate);
|
|
59
|
+
if (!start) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const zone = invite.allDay ? allDayZone(invite.timezone) : undefined;
|
|
63
|
+
const weekday = new Intl.DateTimeFormat(undefined, { weekday: "short", timeZone: zone }).format(start);
|
|
64
|
+
const date = new Intl.DateTimeFormat(undefined, { year: "numeric", month: "numeric", day: "numeric", timeZone: zone }).format(start);
|
|
65
|
+
return invite.allDay ? `${weekday} ${date}` : `${weekday} ${date} ${new Intl.DateTimeFormat(undefined, CLOCK_FORMAT).format(start)}`;
|
|
66
|
+
}
|
|
67
|
+
/** `YYYY-MM-DD` of an instant in the reader's zone, or in `timeZone` when given (`en-CA` writes a date that way). */
|
|
68
|
+
export function isoDay(date, timeZone) {
|
|
69
|
+
return new Intl.DateTimeFormat("en-CA", { year: "numeric", month: "2-digit", day: "2-digit", timeZone }).format(date);
|
|
70
|
+
}
|
|
71
|
+
/** Where "Open in Calendar" goes: the day view of the day the meeting starts (the `date`/`view` the calendar page reads), or the calendar itself
|
|
72
|
+
* when the invitation names no usable start. An all-day event's day is the organizer's; any other is the reader's. */
|
|
73
|
+
export function calendarHref(invite) {
|
|
74
|
+
const start = validDate(invite.startDate);
|
|
75
|
+
if (!start) {
|
|
76
|
+
return APP_HREFS.calendar;
|
|
77
|
+
}
|
|
78
|
+
return `${APP_HREFS.calendar}?date=${isoDay(start, invite.allDay ? allDayZone(invite.timezone) : undefined)}&view=day`;
|
|
79
|
+
}
|
|
80
|
+
/** The reader's answer as a list row says it. */
|
|
81
|
+
export const ANSWER_LABEL = {
|
|
82
|
+
accepted: "Accepted",
|
|
83
|
+
tentative: "Tentative",
|
|
84
|
+
declined: "Declined",
|
|
85
|
+
};
|
|
86
|
+
/** The most conflicting events that are named; more are counted. */
|
|
87
|
+
const MAX_NAMED_CONFLICTS = 3;
|
|
88
|
+
/** The titles of the events an invitation conflicts with, as "A, B and 2 more". */
|
|
89
|
+
export function conflictSummary(conflicts) {
|
|
90
|
+
const titles = conflicts.map((c) => c.title.trim() || "(no title)");
|
|
91
|
+
return titles.length <= MAX_NAMED_CONFLICTS
|
|
92
|
+
? titles.join(", ")
|
|
93
|
+
: `${titles.slice(0, MAX_NAMED_CONFLICTS).join(", ")} and ${titles.length - MAX_NAMED_CONFLICTS} more`;
|
|
94
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MessageInvite } from "@rapidmx/react-shared/calendar/inviteApi.js";
|
|
2
|
+
/**
|
|
3
|
+
* A per-message cache of the calendar invitation the server reads out of a message, shared by everything that draws one: the reading pane's
|
|
4
|
+
* card, the list row's RSVP chip and its popover. They ask for the same message a moment apart (the row's chip, then the pane once it is
|
|
5
|
+
* opened), and an answer given in one has to show in the others, so a lookup is made once and every answer goes through `storeInvite()`.
|
|
6
|
+
*
|
|
7
|
+
* A message with no invitation (`null`, the server's 404) is remembered too. A lookup that fails is not - nothing is cached and the next
|
|
8
|
+
* thing to ask (a remount, another row) tries again - but nothing retries on its own. An entry goes stale after `INVITE_TTL_MS`, because the
|
|
9
|
+
* calendar it describes can change elsewhere (the meeting deleted, another device answering); it is refetched when next asked for.
|
|
10
|
+
*/
|
|
11
|
+
/** How long a looked-up invitation is trusted before the next ask reads it again. */
|
|
12
|
+
export declare const INVITE_TTL_MS = 30000;
|
|
13
|
+
/** Reads a message's invitation, from the cache when it is fresh. Rejects when the lookup fails. */
|
|
14
|
+
export declare function loadInvite(uid: string): Promise<MessageInvite | null>;
|
|
15
|
+
/** Records the invitation as it now stands after an answer, so the pane, the chip and the popover all show it. */
|
|
16
|
+
export declare function storeInvite(uid: string, invite: MessageInvite): void;
|
|
17
|
+
/** Forgets everything - for tests, and for a sign-out. */
|
|
18
|
+
export declare function clearInviteCache(): void;
|
|
19
|
+
/**
|
|
20
|
+
* A message's invitation, looked up once (shared with every other user of the same message) when `enabled`. `invite` is `null` until it is
|
|
21
|
+
* known, and stays `null` for a message with none or a lookup that failed - nothing to draw either way. `settled` says the lookup is over.
|
|
22
|
+
* Nothing is asked while disabled, and a failure is never retried from here.
|
|
23
|
+
*/
|
|
24
|
+
export declare function useMessageInvite(uid: string, enabled?: boolean): {
|
|
25
|
+
invite: MessageInvite | null;
|
|
26
|
+
settled: boolean;
|
|
27
|
+
setInvite: (invite: MessageInvite) => void;
|
|
28
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useCallback, useEffect, useReducer } from "react";
|
|
6
|
+
import { getMessageInvite } from "@rapidmx/react-shared/calendar/inviteApi.js";
|
|
7
|
+
/**
|
|
8
|
+
* A per-message cache of the calendar invitation the server reads out of a message, shared by everything that draws one: the reading pane's
|
|
9
|
+
* card, the list row's RSVP chip and its popover. They ask for the same message a moment apart (the row's chip, then the pane once it is
|
|
10
|
+
* opened), and an answer given in one has to show in the others, so a lookup is made once and every answer goes through `storeInvite()`.
|
|
11
|
+
*
|
|
12
|
+
* A message with no invitation (`null`, the server's 404) is remembered too. A lookup that fails is not - nothing is cached and the next
|
|
13
|
+
* thing to ask (a remount, another row) tries again - but nothing retries on its own. An entry goes stale after `INVITE_TTL_MS`, because the
|
|
14
|
+
* calendar it describes can change elsewhere (the meeting deleted, another device answering); it is refetched when next asked for.
|
|
15
|
+
*/
|
|
16
|
+
/** How long a looked-up invitation is trusted before the next ask reads it again. */
|
|
17
|
+
export const INVITE_TTL_MS = 30000;
|
|
18
|
+
const entries = new Map();
|
|
19
|
+
const listeners = new Map();
|
|
20
|
+
function notify(uid) {
|
|
21
|
+
listeners.get(uid)?.forEach((listener) => listener());
|
|
22
|
+
}
|
|
23
|
+
/** Older servers, or a partial answer, may leave a list out: a card must never fail to draw over one. */
|
|
24
|
+
function normalize(invite) {
|
|
25
|
+
if (!invite) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const partial = invite;
|
|
29
|
+
return { ...invite, attendees: partial.attendees ?? [], conflicts: partial.conflicts ?? [], schedule: partial.schedule ?? [] };
|
|
30
|
+
}
|
|
31
|
+
/** Reads a message's invitation, from the cache when it is fresh. Rejects when the lookup fails. */
|
|
32
|
+
export function loadInvite(uid) {
|
|
33
|
+
const existing = entries.get(uid);
|
|
34
|
+
if (existing && (existing.value === undefined || Date.now() - existing.at < INVITE_TTL_MS)) {
|
|
35
|
+
return existing.promise;
|
|
36
|
+
}
|
|
37
|
+
const entry = {
|
|
38
|
+
at: Date.now(),
|
|
39
|
+
promise: getMessageInvite(uid).then((invite) => {
|
|
40
|
+
entry.value = normalize(invite);
|
|
41
|
+
entry.at = Date.now();
|
|
42
|
+
notify(uid);
|
|
43
|
+
return entry.value;
|
|
44
|
+
}, (err) => {
|
|
45
|
+
if (entries.get(uid) === entry) {
|
|
46
|
+
entries.delete(uid);
|
|
47
|
+
}
|
|
48
|
+
throw err;
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
entries.set(uid, entry);
|
|
52
|
+
return entry.promise;
|
|
53
|
+
}
|
|
54
|
+
/** Records the invitation as it now stands after an answer, so the pane, the chip and the popover all show it. */
|
|
55
|
+
export function storeInvite(uid, invite) {
|
|
56
|
+
const value = normalize(invite);
|
|
57
|
+
entries.set(uid, { promise: Promise.resolve(value), value, at: Date.now() });
|
|
58
|
+
notify(uid);
|
|
59
|
+
}
|
|
60
|
+
/** Forgets everything - for tests, and for a sign-out. */
|
|
61
|
+
export function clearInviteCache() {
|
|
62
|
+
entries.clear();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A message's invitation, looked up once (shared with every other user of the same message) when `enabled`. `invite` is `null` until it is
|
|
66
|
+
* known, and stays `null` for a message with none or a lookup that failed - nothing to draw either way. `settled` says the lookup is over.
|
|
67
|
+
* Nothing is asked while disabled, and a failure is never retried from here.
|
|
68
|
+
*/
|
|
69
|
+
export function useMessageInvite(uid, enabled = true) {
|
|
70
|
+
const [, rerender] = useReducer((n) => n + 1, 0);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!enabled) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let cancelled = false;
|
|
76
|
+
const update = () => {
|
|
77
|
+
if (!cancelled) {
|
|
78
|
+
rerender();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const set = listeners.get(uid) ?? new Set();
|
|
82
|
+
set.add(update);
|
|
83
|
+
listeners.set(uid, set);
|
|
84
|
+
loadInvite(uid).then(update, () => {
|
|
85
|
+
// Nothing to show, and nothing worth interrupting the reader with: the message reads the same without it.
|
|
86
|
+
});
|
|
87
|
+
return () => {
|
|
88
|
+
cancelled = true;
|
|
89
|
+
set.delete(update);
|
|
90
|
+
};
|
|
91
|
+
}, [uid, enabled]);
|
|
92
|
+
const setInvite = useCallback((invite) => storeInvite(uid, invite), [uid]);
|
|
93
|
+
const value = enabled ? entries.get(uid)?.value : undefined;
|
|
94
|
+
return { invite: value ?? null, settled: value !== undefined, setInvite };
|
|
95
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { FolderBadge } from "../../../mail/folderCounts.js";
|
|
3
|
+
/** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
|
|
4
|
+
export default function FolderBadgeChip({ badge }: {
|
|
5
|
+
badge: FolderBadge;
|
|
6
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { badgeLabel } from "../../../mail/folderCounts.js";
|
|
3
|
+
/** A folder's badge: an accent pill with the unread count, or - for the folders that show how many they hold - plain muted text. */
|
|
4
|
+
export default function FolderBadgeChip({ badge }) {
|
|
5
|
+
return (_jsxs("span", { className: [
|
|
6
|
+
"text-xs rounded-pill py-0.5 px-1.5",
|
|
7
|
+
badge.kind === "unread" ? "font-bold bg-primary/15 text-primary-dark" : "font-medium text-text-muted",
|
|
8
|
+
].join(" "), children: [_jsx("span", { "aria-hidden": "true", children: badge.value }), _jsxs("span", { className: "sr-only", children: [" ", badgeLabel(badge)] })] }));
|
|
9
|
+
}
|
|
@@ -59,6 +59,11 @@ export interface MailShellContextValue {
|
|
|
59
59
|
* context value, which is only ever read outside a real shell.
|
|
60
60
|
*/
|
|
61
61
|
trackMessageChange: (previous: Message, next: Message | null) => CountTracker;
|
|
62
|
+
/**
|
|
63
|
+
* The phone layout's header row beside the folders button, for the page to put its search box in (`createPortal()`): the shell
|
|
64
|
+
* owns the row, the page owns the search state. `null` until the row has rendered, and on the default context value.
|
|
65
|
+
*/
|
|
66
|
+
mobileSearchSlot: HTMLElement | null;
|
|
62
67
|
}
|
|
63
68
|
/** Reads the mailbox/folder a page is currently showing, as resolved by the enclosing `MailShell`. */
|
|
64
69
|
export declare function useMailShell(): MailShellContextValue;
|
|
@@ -78,5 +83,10 @@ export { MAILBOX_LIST_LIMIT };
|
|
|
78
83
|
* behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
|
|
79
84
|
* aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
|
|
80
85
|
* of folders" requirement.
|
|
86
|
+
*
|
|
87
|
+
* An address that names nothing opens "All Mailboxes > Inbox" when there is more than one mailbox (that is when the section exists), and
|
|
88
|
+
* the one mailbox's own Inbox otherwise; a `?mailboxUid=`, `?folderUid=` or `?aggregate=` always wins. Nothing is selected until the address
|
|
89
|
+
* has been read. What needs a single mailbox while an all-mailboxes view is open (Compose, the new-message shortcut, key unlocking) uses the
|
|
90
|
+
* primary mailbox.
|
|
81
91
|
*/
|
|
82
92
|
export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }: PropsWithChildren<MailShellProps>): React.JSX.Element;
|