@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
|
@@ -3,16 +3,19 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
6
|
+
import { flushSync } from "react-dom";
|
|
6
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
8
|
import { Attachment, Folder, Message, listAttachments } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
8
9
|
import { ConversationSummary, listConversationMessages } from "@rapidmx/react-shared/mail/conversationsApi.js";
|
|
9
10
|
import { Label } from "@rapidmx/react-shared/mail/labelsApi.js";
|
|
10
11
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
12
|
import MessageDetailPane from "./MessageDetailPane.js";
|
|
12
|
-
import { EncryptedPreview, displaySubject } from "./reading/EncryptedPreview.js";
|
|
13
|
+
import { EncryptedPreview, displaySubject } from "./reading/EncryptedPreview.js";
|
|
13
14
|
import { CollapsedCard, SkeletonCards, SubjectCard } from "./reading/MessageCard.js";
|
|
15
|
+
import PendingMessageCard from "./reading/PendingMessageCard.js";
|
|
14
16
|
import { useMailShell } from "./layout/MailShell.js";
|
|
15
17
|
import { setReadState } from "../../mail/messageReadState.js";
|
|
18
|
+
import { adoptedOutgoing, belongsToThread, settleOutgoing, useOutgoingReplies } from "../../mail/outbox/outgoingReplies.js";
|
|
16
19
|
import { dateClass, isUnread, senderClass } from "./unreadStyle.js";
|
|
17
20
|
|
|
18
21
|
/** One request's worth of the thread. The server's own default for `listConversationMessages()`. */
|
|
@@ -25,8 +28,12 @@ export const THREAD_PAGE_SIZE = 100;
|
|
|
25
28
|
*/
|
|
26
29
|
export const THREAD_MESSAGE_LIMIT = 500;
|
|
27
30
|
|
|
31
|
+
/** What the pane needs to know of a conversation up front: the thread itself is loaded by `conversationId`. The subject and
|
|
32
|
+
* count fill the header while it loads, so a caller that only has a message (the mobile route) gives its subject and a count of 1. */
|
|
33
|
+
export type ConversationThreadHead = Pick<ConversationSummary, "conversationId" | "subject" | "messageCount">;
|
|
34
|
+
|
|
28
35
|
export interface ConversationThreadPaneProps {
|
|
29
|
-
conversation:
|
|
36
|
+
conversation: ConversationThreadHead | null;
|
|
30
37
|
/** The mailbox the conversation was listed from - `listConversationMessages()` is mailbox-scoped. */
|
|
31
38
|
mailboxUid: string;
|
|
32
39
|
/**
|
|
@@ -117,6 +124,21 @@ async function loadThread(mailboxUid: string, conversationId: string): Promise<{
|
|
|
117
124
|
return { messages, truncated: more };
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
/**
|
|
128
|
+
* `next` - the thread as it was just read again - for the pane to show in place of `previous`. A message the pane holds a newer copy of (one that
|
|
129
|
+
* was read, flagged or labelled here a moment ago, whose change the read may have started before) keeps that copy, and a read that changed
|
|
130
|
+
* nothing gives back `previous` itself, so the pane is not drawn again.
|
|
131
|
+
*/
|
|
132
|
+
function mergeThread(previous: Message[], next: Message[]): Message[] {
|
|
133
|
+
const held = new Map(previous.map((message) => [message.uid, message]));
|
|
134
|
+
const merged = next.map((message) => {
|
|
135
|
+
const own = held.get(message.uid);
|
|
136
|
+
return own && own.version > message.version ? own : message;
|
|
137
|
+
});
|
|
138
|
+
const unchanged = merged.length === previous.length && merged.every((message, index) => message.uid === previous[index].uid && message.version === previous[index].version);
|
|
139
|
+
return unchanged ? previous : merged;
|
|
140
|
+
}
|
|
141
|
+
|
|
120
142
|
/** Whether the browser is showing a focus ring on `element` - it was focused from the keyboard (or by a key press's handler), not by a click. */
|
|
121
143
|
function hasFocusRing(element: HTMLElement): boolean {
|
|
122
144
|
try {
|
|
@@ -156,7 +178,9 @@ export default function ConversationThreadPane({
|
|
|
156
178
|
onFolderCreated,
|
|
157
179
|
shortcuts,
|
|
158
180
|
}: ConversationThreadPaneProps) {
|
|
159
|
-
const { trackMessageChange } = useMailShell();
|
|
181
|
+
const { trackMessageChange, live } = useMailShell();
|
|
182
|
+
// The replies and forwards this tab has sent, drawn at the top of the thread they continue until the server's own copy is in it.
|
|
183
|
+
const outgoing = useOutgoingReplies();
|
|
160
184
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
161
185
|
const [attachmentsByUid, setAttachmentsByUid] = useState<Record<string, Attachment[]>>({});
|
|
162
186
|
const [expandedUids, setExpandedUids] = useState<Set<string>>(new Set());
|
|
@@ -179,13 +203,19 @@ export default function ConversationThreadPane({
|
|
|
179
203
|
* expansion run below must sit that render out rather than anchor on a message from another thread. */
|
|
180
204
|
const loadedIdRef = useRef<string | null>(null);
|
|
181
205
|
const rowRefs = useRef<Record<string, HTMLLIElement | null>>({});
|
|
182
|
-
const headerRefs = useRef<Record<string,
|
|
206
|
+
const headerRefs = useRef<Record<string, HTMLElement | null>>({});
|
|
183
207
|
/** Where the toggled message's header sat in the viewport before it expanded, so the run below can put
|
|
184
208
|
* it back there - expanding a message above the one being read must not shove that one off-screen. */
|
|
185
209
|
const anchorRef = useRef<{ uid: string; top: number } | null>(null);
|
|
186
210
|
/** The message whose header button had the focus when it was toggled: expanding or collapsing swaps that button for the other state's, so the
|
|
187
211
|
* focus is put back on the new one. */
|
|
188
212
|
const refocusRef = useRef<string | null>(null);
|
|
213
|
+
/** The pending cards that have already been scrolled to, so that only a message that has just been sent takes the view and the focus. */
|
|
214
|
+
const announcedRef = useRef<Set<string>>(new Set());
|
|
215
|
+
/** Messages that left the thread here (archived, moved, a scheduled send taken back): a read of the thread again still lists them, since a conversation spans folders. */
|
|
216
|
+
const removedRef = useRef<Set<string>>(new Set());
|
|
217
|
+
/** The last live update this pane has answered - one already in the shell's hands when the pane opened is not news. */
|
|
218
|
+
const seenLiveRef = useRef(live);
|
|
189
219
|
|
|
190
220
|
const conversationId = conversation?.conversationId;
|
|
191
221
|
|
|
@@ -193,6 +223,8 @@ export default function ConversationThreadPane({
|
|
|
193
223
|
const generation = ++generationRef.current;
|
|
194
224
|
markReadRequestedRef.current = new Set();
|
|
195
225
|
attachmentsRequestedRef.current = new Set();
|
|
226
|
+
announcedRef.current = new Set();
|
|
227
|
+
removedRef.current = new Set();
|
|
196
228
|
appliedSelectionRef.current = null;
|
|
197
229
|
loadedIdRef.current = null;
|
|
198
230
|
setMessages([]);
|
|
@@ -212,6 +244,8 @@ export default function ConversationThreadPane({
|
|
|
212
244
|
.then((loaded) => {
|
|
213
245
|
if (generation !== generationRef.current) return;
|
|
214
246
|
loadedIdRef.current = conversationId;
|
|
247
|
+
// A message sent from here whose Sent Items copy is already in the thread is drawn as that copy, not as a pending card.
|
|
248
|
+
settleOutgoing(mailboxUid, loaded.messages);
|
|
215
249
|
setMessages(loaded.messages);
|
|
216
250
|
setTruncated(loaded.truncated);
|
|
217
251
|
})
|
|
@@ -334,6 +368,80 @@ export default function ConversationThreadPane({
|
|
|
334
368
|
}
|
|
335
369
|
}, [expandedUids, messages]);
|
|
336
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Reads the thread again, quietly: what is on screen stays until the answer is here (a read that fails changes nothing), a message that arrived
|
|
373
|
+
* is added at the top, and the reader's expanded and collapsed cards are left as they are. A message this tab sent - which the server has
|
|
374
|
+
* now filed in Sent Items under the same `uid` - takes the place of its pending card, expanded as that card was, with the focus if the
|
|
375
|
+
* card had it. A thread that has not finished its first load is not read again: that load is the fresh read.
|
|
376
|
+
*/
|
|
377
|
+
function refreshThread() {
|
|
378
|
+
const id = loadedIdRef.current;
|
|
379
|
+
if (!id) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const generation = generationRef.current;
|
|
383
|
+
loadThread(mailboxUid, id).then(
|
|
384
|
+
(loaded) => {
|
|
385
|
+
if (generation !== generationRef.current) return;
|
|
386
|
+
const current = loaded.messages.filter((message) => !removedRef.current.has(message.uid));
|
|
387
|
+
const adopted = adoptedOutgoing(mailboxUid, current);
|
|
388
|
+
const refocus = adopted.find((uid) => document.activeElement === headerRefs.current[uid]);
|
|
389
|
+
// Drawn first, and only then is the pending card forgotten: there is never a frame with neither it nor the real message.
|
|
390
|
+
flushSync(() => {
|
|
391
|
+
setMessages((previous) => mergeThread(previous, current));
|
|
392
|
+
setTruncated(loaded.truncated);
|
|
393
|
+
if (adopted.length > 0) {
|
|
394
|
+
setExpandedUids((previous) => new Set([...previous, ...adopted]));
|
|
395
|
+
}
|
|
396
|
+
if (refocus) {
|
|
397
|
+
setPendingFocusUid(refocus);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
settleOutgoing(mailboxUid, current);
|
|
401
|
+
},
|
|
402
|
+
// Quietly: the next live update reads it again, and what is shown is still right.
|
|
403
|
+
() => undefined,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Another message of this conversation may have arrived - a recipient's reply, one sent from another tab or device, or this tab's own reply
|
|
408
|
+
// filed in Sent Items - whenever the shell announces a live update that touched one of this mailbox's folders (or does not say which).
|
|
409
|
+
useEffect(() => {
|
|
410
|
+
if (live === seenLiveRef.current) return;
|
|
411
|
+
seenLiveRef.current = live;
|
|
412
|
+
if (live.folderUids === null || folders.some((folder) => live.folderUids!.has(folder.uid))) {
|
|
413
|
+
refreshThread();
|
|
414
|
+
}
|
|
415
|
+
}, [live]);
|
|
416
|
+
|
|
417
|
+
// The server has relayed a message this tab sent: its Sent Items copy is read for at once, without waiting for the live update that follows.
|
|
418
|
+
const sentKey = outgoing
|
|
419
|
+
.filter((reply) => reply.state === "sent" && reply.mailboxUid === mailboxUid)
|
|
420
|
+
.map((reply) => reply.uid)
|
|
421
|
+
.join(",");
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
if (sentKey) {
|
|
424
|
+
refreshThread();
|
|
425
|
+
}
|
|
426
|
+
}, [sentKey]);
|
|
427
|
+
|
|
428
|
+
// The messages sent from here that continue this thread and whose real copy it does not hold yet, newest first (the pane's order). A message
|
|
429
|
+
// that replies to nothing here - a new message, or a reply to another conversation - is not in this list, and never is.
|
|
430
|
+
const pendingCards = outgoing
|
|
431
|
+
.filter((reply) => reply.mailboxUid === mailboxUid && !messages.some((message) => message.uid === reply.uid) && belongsToThread(reply, messages))
|
|
432
|
+
.reverse();
|
|
433
|
+
// A message that has just been sent is scrolled into view and takes the focus - the compose window it was sent from has just closed, and the focus
|
|
434
|
+
// with it - once. A failed one, or one that was already there when the thread was opened, is left where it is.
|
|
435
|
+
const pendingKey = pendingCards.map((reply) => reply.uid).join(",");
|
|
436
|
+
useEffect(() => {
|
|
437
|
+
const fresh = pendingCards.filter((reply) => reply.state === "sending" && !announcedRef.current.has(reply.uid));
|
|
438
|
+
if (fresh.length === 0) return;
|
|
439
|
+
for (const reply of fresh) {
|
|
440
|
+
announcedRef.current.add(reply.uid);
|
|
441
|
+
}
|
|
442
|
+
setPendingFocusUid(fresh[0].uid);
|
|
443
|
+
}, [pendingKey]);
|
|
444
|
+
|
|
337
445
|
/** A newer copy of one of the thread's messages, kept here and handed to the list - with the copy it
|
|
338
446
|
* replaces where the caller was given one, so a conversation row can tell what actually changed. */
|
|
339
447
|
function patchMessage(updated: Message, previous?: Message) {
|
|
@@ -343,6 +451,7 @@ export default function ConversationThreadPane({
|
|
|
343
451
|
|
|
344
452
|
/** A message that left the folder being listed - it leaves the thread too, as it left the list. */
|
|
345
453
|
function removeMessage(updated: Message) {
|
|
454
|
+
removedRef.current.add(updated.uid);
|
|
346
455
|
setMessages((prev) => prev.filter((message) => message.uid !== updated.uid));
|
|
347
456
|
onMessageRemoved(updated);
|
|
348
457
|
}
|
|
@@ -395,7 +504,7 @@ export default function ConversationThreadPane({
|
|
|
395
504
|
: undefined
|
|
396
505
|
: error
|
|
397
506
|
? undefined
|
|
398
|
-
: `${messages.length} message${messages.length === 1 ? "" : "s"}`
|
|
507
|
+
: `${messages.length + pendingCards.length} message${messages.length + pendingCards.length === 1 ? "" : "s"}`
|
|
399
508
|
}
|
|
400
509
|
>
|
|
401
510
|
{truncated && !loading && !error && (
|
|
@@ -413,6 +522,22 @@ export default function ConversationThreadPane({
|
|
|
413
522
|
</div>
|
|
414
523
|
) : (
|
|
415
524
|
<ul className="flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3">
|
|
525
|
+
{pendingCards.map((reply) => (
|
|
526
|
+
<li
|
|
527
|
+
key={reply.uid}
|
|
528
|
+
ref={(node) => {
|
|
529
|
+
rowRefs.current[reply.uid] = node;
|
|
530
|
+
}}
|
|
531
|
+
data-outgoing="true"
|
|
532
|
+
>
|
|
533
|
+
<PendingMessageCard
|
|
534
|
+
reply={reply}
|
|
535
|
+
headerRef={(node) => {
|
|
536
|
+
headerRefs.current[reply.uid] = node;
|
|
537
|
+
}}
|
|
538
|
+
/>
|
|
539
|
+
</li>
|
|
540
|
+
))}
|
|
416
541
|
{messages.map((message) => {
|
|
417
542
|
const uid = message.uid;
|
|
418
543
|
const expanded = expandedUids.has(uid);
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useId, useState } from "react";
|
|
6
|
+
import { HiOutlineCalendarDays, HiOutlineCheck } from "react-icons/hi2";
|
|
7
|
+
import {
|
|
8
|
+
InviteParticipant,
|
|
9
|
+
InviteResponse,
|
|
10
|
+
MessageInvite,
|
|
11
|
+
ProposedTime,
|
|
12
|
+
acceptProposal,
|
|
13
|
+
proposeNewTime,
|
|
14
|
+
removeMessageInvite,
|
|
15
|
+
respondToMessageInvite,
|
|
16
|
+
} from "@rapidmx/react-shared/calendar/inviteApi.js";
|
|
17
|
+
import { guestPermissionsOf } from "@rapidmx/react-shared/calendar/calendarApi.js";
|
|
18
|
+
import { formatMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
|
|
19
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
20
|
+
import EventDescriptionView from "../calendar/EventDescriptionView.js";
|
|
21
|
+
import RequestChangeForm from "../calendar/RequestChangeForm.js";
|
|
22
|
+
import { VISIBILITY_LABEL, describeGuestPermissions } from "../calendar/eventFormat.js";
|
|
23
|
+
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
24
|
+
import { calendarHref, conflictSummary, formatInviteWhen } from "./invite/inviteFormat.js";
|
|
25
|
+
import { useMessageInvite } from "./invite/inviteStore.js";
|
|
26
|
+
import ProposeTimeForm from "./invite/ProposeTimeForm.js";
|
|
27
|
+
|
|
28
|
+
export { formatInviteWhen, isCalendarAttachment } from "./invite/inviteFormat.js";
|
|
29
|
+
|
|
30
|
+
const HEADINGS: Record<string, string> = {
|
|
31
|
+
REQUEST: "Meeting invitation",
|
|
32
|
+
CANCEL: "Meeting canceled",
|
|
33
|
+
REPLY: "Meeting response",
|
|
34
|
+
COUNTER: "New time proposed",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The heading of a `COUNTER` that is a guest's request for a change (rather than a different time). */
|
|
38
|
+
const CHANGE_REQUEST_HEADING = "Change requested";
|
|
39
|
+
|
|
40
|
+
/** How an answer reads in "Bob accepted." (a REPLY) - a participant who has not answered has not answered. */
|
|
41
|
+
const REPLY_VERBS: Partial<Record<NonNullable<InviteParticipant["responseStatus"]>, string>> = {
|
|
42
|
+
accepted: "accepted",
|
|
43
|
+
tentative: "tentatively accepted",
|
|
44
|
+
declined: "declined",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** The reader's own answer, as the line under the invitation says it. */
|
|
48
|
+
const ANSWER_TEXT: Record<InviteResponse, string> = {
|
|
49
|
+
accepted: "You accepted this meeting.",
|
|
50
|
+
tentative: "You tentatively accepted this meeting.",
|
|
51
|
+
declined: "You declined this meeting. It is not on your calendar.",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const ANSWER_CLASS: Record<InviteResponse, string> = {
|
|
55
|
+
accepted: "text-success",
|
|
56
|
+
tentative: "text-text",
|
|
57
|
+
declined: "text-danger",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const RESPONSES: { response: InviteResponse; label: string; failure: string }[] = [
|
|
61
|
+
{ response: "accepted", label: "Accept", failure: "Couldn't accept this meeting" },
|
|
62
|
+
{ response: "tentative", label: "Tentative", failure: "Couldn't respond tentatively to this meeting" },
|
|
63
|
+
{ response: "declined", label: "Decline", failure: "Couldn't decline this meeting" },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** The most attendees that are listed by name; a longer list is only counted. */
|
|
67
|
+
const MAX_NAMED_ATTENDEES = 5;
|
|
68
|
+
|
|
69
|
+
/** What the card is doing for the reader right now: an answer being sent, an add/remove, a proposal, an accepted proposal. */
|
|
70
|
+
type Pending = InviteResponse | "add" | "remove" | "propose" | "acceptProposal";
|
|
71
|
+
|
|
72
|
+
function attendeeSummary(attendees: InviteParticipant[]): string | undefined {
|
|
73
|
+
if (attendees.length === 0) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const count = `${attendees.length} ${attendees.length === 1 ? "attendee" : "attendees"}`;
|
|
77
|
+
return attendees.length <= MAX_NAMED_ATTENDEES ? `${count}: ${attendees.map((a) => a.displayName || a.address).join(", ")}` : count;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface InviteCardProps {
|
|
81
|
+
/** The message whose calendar invitation to show, if it carries one. Nothing is drawn until the server says there is one. */
|
|
82
|
+
messageUid: string;
|
|
83
|
+
/** The level of the card's heading, so it sits below whatever heading its surroundings have (`2` in the reading pane, `3` inside a thread's card). */
|
|
84
|
+
headingLevel?: 2 | 3;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The calendar invitation a message carries, as Outlook and Gmail draw it: what the meeting is, when and where, who organizes it, and the buttons
|
|
89
|
+
* that answer it - Accept, Tentative, Decline (and Propose new time) for a request; "Add to calendar" for a published event; "Remove from
|
|
90
|
+
* calendar" for a cancellation; "Accept proposal" for an attendee's proposed new time. An attendee's answer to the reader's own invitation is
|
|
91
|
+
* one line ("Bob tentatively accepted."). Everything that follows an action (the meeting going onto the calendar or off it, the reply to the
|
|
92
|
+
* organizer) is the server's; this only asks for it and draws the invitation the server sends back.
|
|
93
|
+
*
|
|
94
|
+
* Asks the server once per message (through the shared invitation cache, so the list row's RSVP chip and this card agree). A message with no readable
|
|
95
|
+
* invitation, and a lookup that fails for any reason, draw nothing - a lookup that fails is never retried and never disturbs the message around it.
|
|
96
|
+
* An answer that arrives after the message changed or this was unmounted is dropped.
|
|
97
|
+
*/
|
|
98
|
+
export default function InviteCard({ messageUid, headingLevel = 2 }: InviteCardProps) {
|
|
99
|
+
const { invite, setInvite } = useMessageInvite(messageUid);
|
|
100
|
+
const [pending, setPending] = useState<Pending | null>(null);
|
|
101
|
+
const [proposing, setProposing] = useState(false);
|
|
102
|
+
// Said once after a proposal went out: the invitation itself doesn't change.
|
|
103
|
+
const [proposed, setProposed] = useState(false);
|
|
104
|
+
// A guest's request for a change (or for guests to be added): the form is open, and once it went out.
|
|
105
|
+
const [requesting, setRequesting] = useState(false);
|
|
106
|
+
const [requested, setRequested] = useState(false);
|
|
107
|
+
const headingId = useId();
|
|
108
|
+
|
|
109
|
+
if (!invite) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Runs one of the card's actions: the buttons are off while it does, its result replaces what the card shows, and a failure leaves the card as it was. */
|
|
114
|
+
async function run(action: Pending, failure: string, call: () => Promise<MessageInvite>): Promise<boolean> {
|
|
115
|
+
setPending(action);
|
|
116
|
+
try {
|
|
117
|
+
setInvite(await call());
|
|
118
|
+
return true;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
notifyApiError(err, failure);
|
|
121
|
+
return false;
|
|
122
|
+
} finally {
|
|
123
|
+
setPending(null);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function sendProposal(proposal: ProposedTime) {
|
|
128
|
+
if (await run("propose", "Couldn't send the proposed time", () => proposeNewTime(messageUid, proposal))) {
|
|
129
|
+
setProposing(false);
|
|
130
|
+
setProposed(true);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const busy = pending !== null;
|
|
135
|
+
const Heading = `h${headingLevel}` as const;
|
|
136
|
+
const method = invite.method.toUpperCase();
|
|
137
|
+
const isReply = method === "REPLY";
|
|
138
|
+
const isCounter = method === "COUNTER";
|
|
139
|
+
// A `COUNTER` a guest sent to change the event (not just its time): whether the organizer's server applied it already.
|
|
140
|
+
const changeRequest = isCounter ? invite.changeRequest : undefined;
|
|
141
|
+
const permissions = guestPermissionsOf(invite.guestPermissions ?? {});
|
|
142
|
+
const hiddenList = !invite.isOrganizer && !permissions.guestsCanSeeGuestList;
|
|
143
|
+
const canRequest = !!invite.calendarEventUid && (!!invite.canRequestChange || !!invite.canRequestInvite);
|
|
144
|
+
const title = invite.summary?.trim() || "(no title)";
|
|
145
|
+
const when = formatInviteWhen(invite);
|
|
146
|
+
const attendees = hiddenList ? "The organizer has hidden the guest list" : attendeeSummary(invite.attendees);
|
|
147
|
+
const hasDescription = !!(invite.descriptionHtml?.trim() || invite.description?.trim());
|
|
148
|
+
const visibility = invite.visibility && invite.visibility !== "default" ? VISIBILITY_LABEL[invite.visibility] : undefined;
|
|
149
|
+
const sender = invite.reply ?? invite.attendees[0];
|
|
150
|
+
const senderName = sender ? sender.displayName || sender.address : "Someone";
|
|
151
|
+
const showResponses = invite.canRespond && !invite.isOrganizer;
|
|
152
|
+
const conflicts = !isReply && !isCounter && method !== "CANCEL" && invite.conflicts.length > 0 ? conflictSummary(invite.conflicts) : undefined;
|
|
153
|
+
const hasActions =
|
|
154
|
+
showResponses ||
|
|
155
|
+
invite.canPropose ||
|
|
156
|
+
invite.canAcceptProposal ||
|
|
157
|
+
invite.canAdd ||
|
|
158
|
+
invite.canRemove ||
|
|
159
|
+
canRequest ||
|
|
160
|
+
(invite.onCalendar && !!invite.calendarEventUid);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<section aria-labelledby={headingId} className="rounded-sm border border-border bg-surface-alt px-3 py-3 flex flex-col gap-2 text-sm text-text">
|
|
164
|
+
<Heading id={headingId} className="flex items-center gap-2 text-sm font-semibold">
|
|
165
|
+
<HiOutlineCalendarDays size={16} aria-hidden="true" className="shrink-0" />
|
|
166
|
+
{changeRequest ? CHANGE_REQUEST_HEADING : (HEADINGS[method] ?? "Calendar event")}
|
|
167
|
+
</Heading>
|
|
168
|
+
{isReply && (
|
|
169
|
+
// Someone answering the reader's own invitation: a line of news, nothing to do.
|
|
170
|
+
<p className="break-words">
|
|
171
|
+
<span className="font-semibold">{senderName}</span> {(sender?.responseStatus && REPLY_VERBS[sender.responseStatus]) ?? "responded"}.
|
|
172
|
+
</p>
|
|
173
|
+
)}
|
|
174
|
+
{isCounter && !changeRequest && (
|
|
175
|
+
<p className="break-words">
|
|
176
|
+
<span className="font-semibold">{senderName}</span> proposed a new time{when ? <>: {when}</> : "."}
|
|
177
|
+
</p>
|
|
178
|
+
)}
|
|
179
|
+
{changeRequest && (
|
|
180
|
+
<p className="break-words">
|
|
181
|
+
{changeRequest.applied ? (
|
|
182
|
+
<>
|
|
183
|
+
<span className="font-semibold">{senderName}</span>’s change was applied.
|
|
184
|
+
</>
|
|
185
|
+
) : (
|
|
186
|
+
<>
|
|
187
|
+
<span className="font-semibold">{senderName}</span> requested a change.
|
|
188
|
+
</>
|
|
189
|
+
)}
|
|
190
|
+
</p>
|
|
191
|
+
)}
|
|
192
|
+
<p className="font-medium break-words">{title}</p>
|
|
193
|
+
{!isReply && (
|
|
194
|
+
<dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1">
|
|
195
|
+
{when && (!isCounter || !!changeRequest) && (
|
|
196
|
+
<>
|
|
197
|
+
<dt className="text-text-muted">When</dt>
|
|
198
|
+
<dd className="break-words">
|
|
199
|
+
{when}
|
|
200
|
+
{invite.recurring && <span className="text-text-muted"> (Repeats)</span>}
|
|
201
|
+
</dd>
|
|
202
|
+
</>
|
|
203
|
+
)}
|
|
204
|
+
{invite.location && (
|
|
205
|
+
<>
|
|
206
|
+
<dt className="text-text-muted">Where</dt>
|
|
207
|
+
<dd className="break-words">{invite.location}</dd>
|
|
208
|
+
</>
|
|
209
|
+
)}
|
|
210
|
+
{hasDescription && (!isCounter || !!changeRequest) && (
|
|
211
|
+
<>
|
|
212
|
+
<dt className="text-text-muted">Description</dt>
|
|
213
|
+
<dd className="break-words">
|
|
214
|
+
<EventDescriptionView html={invite.descriptionHtml} text={invite.description} />
|
|
215
|
+
</dd>
|
|
216
|
+
</>
|
|
217
|
+
)}
|
|
218
|
+
{invite.organizer && !isCounter && (
|
|
219
|
+
<>
|
|
220
|
+
<dt className="text-text-muted">Organizer</dt>
|
|
221
|
+
<dd className="break-words">{formatMailAddress(invite.organizer)}</dd>
|
|
222
|
+
</>
|
|
223
|
+
)}
|
|
224
|
+
{attendees && (!isCounter || !!changeRequest) && (
|
|
225
|
+
<>
|
|
226
|
+
<dt className="text-text-muted">Attendees</dt>
|
|
227
|
+
<dd className="break-words">{attendees}</dd>
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
{visibility && !isCounter && (
|
|
231
|
+
<>
|
|
232
|
+
<dt className="text-text-muted">Visibility</dt>
|
|
233
|
+
<dd className="break-words">{visibility}</dd>
|
|
234
|
+
</>
|
|
235
|
+
)}
|
|
236
|
+
</dl>
|
|
237
|
+
)}
|
|
238
|
+
{isReply && when && <p className="text-text-muted break-words">{when}</p>}
|
|
239
|
+
{conflicts && (
|
|
240
|
+
<p className="text-danger break-words">
|
|
241
|
+
<span className="font-medium">Conflicts with:</span> {conflicts}
|
|
242
|
+
</p>
|
|
243
|
+
)}
|
|
244
|
+
{invite.guestPermissions && !isReply && !isCounter && method !== "CANCEL" && (
|
|
245
|
+
<p className="text-xs text-text-muted break-words">{describeGuestPermissions(permissions)}</p>
|
|
246
|
+
)}
|
|
247
|
+
{invite.isOrganizer && !isReply && !isCounter && <p className="text-text-muted">You are the organizer of this meeting.</p>}
|
|
248
|
+
{method === "CANCEL" && <p className="font-medium text-danger">This meeting was canceled.</p>}
|
|
249
|
+
{invite.response && !invite.isOrganizer && !isReply && !isCounter && (
|
|
250
|
+
<p role="status" className={`font-medium ${ANSWER_CLASS[invite.response]}`}>
|
|
251
|
+
{ANSWER_TEXT[invite.response]}
|
|
252
|
+
</p>
|
|
253
|
+
)}
|
|
254
|
+
{isCounter && invite.response === "accepted" && !changeRequest?.applied && (
|
|
255
|
+
<p role="status" className="font-medium text-success">
|
|
256
|
+
You accepted the proposed time.
|
|
257
|
+
</p>
|
|
258
|
+
)}
|
|
259
|
+
{proposed && (
|
|
260
|
+
<p role="status" className="font-medium text-text">
|
|
261
|
+
Your proposed time was sent to the organizer.
|
|
262
|
+
</p>
|
|
263
|
+
)}
|
|
264
|
+
{requested && (
|
|
265
|
+
<p role="status" className="font-medium text-text">
|
|
266
|
+
Your change was sent to the organizer.
|
|
267
|
+
</p>
|
|
268
|
+
)}
|
|
269
|
+
{invite.outdated && <p className="text-xs text-text-muted">A newer version of this meeting is already on your calendar.</p>}
|
|
270
|
+
{hasActions && (
|
|
271
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
272
|
+
{showResponses && (
|
|
273
|
+
<div role="group" aria-label="Respond to this meeting" aria-busy={busy || undefined} className="flex flex-wrap gap-2">
|
|
274
|
+
{RESPONSES.map(({ response, label, failure }) => {
|
|
275
|
+
const current = invite.response === response;
|
|
276
|
+
return (
|
|
277
|
+
<Button
|
|
278
|
+
key={response}
|
|
279
|
+
type="button"
|
|
280
|
+
variant="secondary"
|
|
281
|
+
className={["!w-auto", current ? "!bg-primary/10 !border-primary disabled:!opacity-100" : ""].join(" ")}
|
|
282
|
+
aria-pressed={current}
|
|
283
|
+
aria-busy={pending === response || undefined}
|
|
284
|
+
loading={pending === response}
|
|
285
|
+
disabled={busy || current}
|
|
286
|
+
onClick={() => void run(response, failure, () => respondToMessageInvite(messageUid, response))}
|
|
287
|
+
>
|
|
288
|
+
{current && <HiOutlineCheck size={14} aria-hidden="true" />}
|
|
289
|
+
{label}
|
|
290
|
+
</Button>
|
|
291
|
+
);
|
|
292
|
+
})}
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
{invite.canPropose && !proposing && (
|
|
296
|
+
<Button type="button" variant="text" disabled={busy} onClick={() => setProposing(true)}>
|
|
297
|
+
Propose new time
|
|
298
|
+
</Button>
|
|
299
|
+
)}
|
|
300
|
+
{canRequest && !requesting && (
|
|
301
|
+
<Button type="button" variant="secondary" className="!w-auto" disabled={busy} onClick={() => setRequesting(true)}>
|
|
302
|
+
{invite.canRequestChange ? "Request a change" : "Add guests"}
|
|
303
|
+
</Button>
|
|
304
|
+
)}
|
|
305
|
+
{invite.canAcceptProposal && invite.response !== "accepted" && (
|
|
306
|
+
<Button
|
|
307
|
+
type="button"
|
|
308
|
+
variant="secondary"
|
|
309
|
+
className="!w-auto"
|
|
310
|
+
aria-busy={pending === "acceptProposal" || undefined}
|
|
311
|
+
loading={pending === "acceptProposal"}
|
|
312
|
+
disabled={busy}
|
|
313
|
+
onClick={() => void run("acceptProposal", "Couldn't accept the proposed time", () => acceptProposal(messageUid))}
|
|
314
|
+
>
|
|
315
|
+
Accept proposal
|
|
316
|
+
</Button>
|
|
317
|
+
)}
|
|
318
|
+
{invite.canAdd && (
|
|
319
|
+
<Button
|
|
320
|
+
type="button"
|
|
321
|
+
variant="secondary"
|
|
322
|
+
className="!w-auto"
|
|
323
|
+
aria-busy={pending === "add" || undefined}
|
|
324
|
+
loading={pending === "add"}
|
|
325
|
+
disabled={busy}
|
|
326
|
+
// A published event has no organizer to answer, so adding it is accepting it: the server sends no reply.
|
|
327
|
+
onClick={() => void run("add", "Couldn't add this event to your calendar", () => respondToMessageInvite(messageUid, "accepted"))}
|
|
328
|
+
>
|
|
329
|
+
Add to calendar
|
|
330
|
+
</Button>
|
|
331
|
+
)}
|
|
332
|
+
{invite.canRemove && (
|
|
333
|
+
<Button
|
|
334
|
+
type="button"
|
|
335
|
+
variant="secondary"
|
|
336
|
+
className="!w-auto"
|
|
337
|
+
aria-busy={pending === "remove" || undefined}
|
|
338
|
+
loading={pending === "remove"}
|
|
339
|
+
disabled={busy}
|
|
340
|
+
onClick={() => void run("remove", "Couldn't remove this meeting from your calendar", () => removeMessageInvite(messageUid))}
|
|
341
|
+
>
|
|
342
|
+
Remove from calendar
|
|
343
|
+
</Button>
|
|
344
|
+
)}
|
|
345
|
+
{invite.onCalendar && invite.calendarEventUid && (
|
|
346
|
+
<a href={calendarHref(invite)} className="text-sm font-medium text-primary-dark hover:underline">
|
|
347
|
+
Open in Calendar
|
|
348
|
+
</a>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
{requesting && (
|
|
353
|
+
<div className="rounded-md bg-surface p-3">
|
|
354
|
+
<RequestChangeForm
|
|
355
|
+
subject={{
|
|
356
|
+
uid: invite.calendarEventUid as string,
|
|
357
|
+
title: invite.summary ?? "",
|
|
358
|
+
location: invite.location,
|
|
359
|
+
startDate: invite.startDate,
|
|
360
|
+
endDate: invite.endDate,
|
|
361
|
+
allDay: invite.allDay,
|
|
362
|
+
recurring: invite.recurring,
|
|
363
|
+
description: invite.description,
|
|
364
|
+
descriptionHtml: invite.descriptionHtml,
|
|
365
|
+
guestAddresses: invite.attendees.map((attendee) => attendee.address),
|
|
366
|
+
}}
|
|
367
|
+
canChange={!!invite.canRequestChange}
|
|
368
|
+
canInvite={!!invite.canRequestInvite}
|
|
369
|
+
onSent={() => {
|
|
370
|
+
setRequesting(false);
|
|
371
|
+
setRequested(true);
|
|
372
|
+
}}
|
|
373
|
+
onCancel={() => setRequesting(false)}
|
|
374
|
+
/>
|
|
375
|
+
</div>
|
|
376
|
+
)}
|
|
377
|
+
{proposing && (
|
|
378
|
+
<ProposeTimeForm
|
|
379
|
+
invite={invite}
|
|
380
|
+
busy={pending === "propose"}
|
|
381
|
+
onSubmit={(proposal) => void sendProposal(proposal)}
|
|
382
|
+
onCancel={() => setProposing(false)}
|
|
383
|
+
/>
|
|
384
|
+
)}
|
|
385
|
+
</section>
|
|
386
|
+
);
|
|
387
|
+
}
|