@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
|
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
7
|
+
import { flushSync } from "react-dom";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import { listAttachments } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
9
10
|
import { listConversationMessages } from "@rapidmx/react-shared/mail/conversationsApi.js";
|
|
@@ -11,8 +12,10 @@ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
|
11
12
|
import MessageDetailPane from "./MessageDetailPane.js";
|
|
12
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
|
/** One request's worth of the thread. The server's own default for `listConversationMessages()`. */
|
|
18
21
|
export const THREAD_PAGE_SIZE = 100;
|
|
@@ -77,6 +80,20 @@ async function loadThread(mailboxUid, conversationId) {
|
|
|
77
80
|
messages.reverse();
|
|
78
81
|
return { messages, truncated: more };
|
|
79
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* `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
|
|
85
|
+
* 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
|
|
86
|
+
* nothing gives back `previous` itself, so the pane is not drawn again.
|
|
87
|
+
*/
|
|
88
|
+
function mergeThread(previous, next) {
|
|
89
|
+
const held = new Map(previous.map((message) => [message.uid, message]));
|
|
90
|
+
const merged = next.map((message) => {
|
|
91
|
+
const own = held.get(message.uid);
|
|
92
|
+
return own && own.version > message.version ? own : message;
|
|
93
|
+
});
|
|
94
|
+
const unchanged = merged.length === previous.length && merged.every((message, index) => message.uid === previous[index].uid && message.version === previous[index].version);
|
|
95
|
+
return unchanged ? previous : merged;
|
|
96
|
+
}
|
|
80
97
|
/** 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. */
|
|
81
98
|
function hasFocusRing(element) {
|
|
82
99
|
try {
|
|
@@ -105,7 +122,9 @@ function hasFocusRing(element) {
|
|
|
105
122
|
* that - mounting a body iframe per message up front would be wasteful in a long thread.
|
|
106
123
|
*/
|
|
107
124
|
export default function ConversationThreadPane({ conversation, mailboxUid, selectedUid, folders, labels, onMessagePatched, onMessageRemoved, onLabelCreated, onFolderCreated, shortcuts, }) {
|
|
108
|
-
const { trackMessageChange } = useMailShell();
|
|
125
|
+
const { trackMessageChange, live } = useMailShell();
|
|
126
|
+
// 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.
|
|
127
|
+
const outgoing = useOutgoingReplies();
|
|
109
128
|
const [messages, setMessages] = useState([]);
|
|
110
129
|
const [attachmentsByUid, setAttachmentsByUid] = useState({});
|
|
111
130
|
const [expandedUids, setExpandedUids] = useState(new Set());
|
|
@@ -134,11 +153,19 @@ export default function ConversationThreadPane({ conversation, mailboxUid, selec
|
|
|
134
153
|
/** 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
|
|
135
154
|
* focus is put back on the new one. */
|
|
136
155
|
const refocusRef = useRef(null);
|
|
156
|
+
/** 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. */
|
|
157
|
+
const announcedRef = useRef(new Set());
|
|
158
|
+
/** 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. */
|
|
159
|
+
const removedRef = useRef(new Set());
|
|
160
|
+
/** The last live update this pane has answered - one already in the shell's hands when the pane opened is not news. */
|
|
161
|
+
const seenLiveRef = useRef(live);
|
|
137
162
|
const conversationId = conversation?.conversationId;
|
|
138
163
|
useEffect(() => {
|
|
139
164
|
const generation = ++generationRef.current;
|
|
140
165
|
markReadRequestedRef.current = new Set();
|
|
141
166
|
attachmentsRequestedRef.current = new Set();
|
|
167
|
+
announcedRef.current = new Set();
|
|
168
|
+
removedRef.current = new Set();
|
|
142
169
|
appliedSelectionRef.current = null;
|
|
143
170
|
loadedIdRef.current = null;
|
|
144
171
|
setMessages([]);
|
|
@@ -159,6 +186,8 @@ export default function ConversationThreadPane({ conversation, mailboxUid, selec
|
|
|
159
186
|
if (generation !== generationRef.current)
|
|
160
187
|
return;
|
|
161
188
|
loadedIdRef.current = conversationId;
|
|
189
|
+
// A message sent from here whose Sent Items copy is already in the thread is drawn as that copy, not as a pending card.
|
|
190
|
+
settleOutgoing(mailboxUid, loaded.messages);
|
|
162
191
|
setMessages(loaded.messages);
|
|
163
192
|
setTruncated(loaded.truncated);
|
|
164
193
|
})
|
|
@@ -283,6 +312,77 @@ export default function ConversationThreadPane({ conversation, mailboxUid, selec
|
|
|
283
312
|
}
|
|
284
313
|
}
|
|
285
314
|
}, [expandedUids, messages]);
|
|
315
|
+
/**
|
|
316
|
+
* 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
|
|
317
|
+
* 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
|
|
318
|
+
* 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
|
|
319
|
+
* card had it. A thread that has not finished its first load is not read again: that load is the fresh read.
|
|
320
|
+
*/
|
|
321
|
+
function refreshThread() {
|
|
322
|
+
const id = loadedIdRef.current;
|
|
323
|
+
if (!id) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const generation = generationRef.current;
|
|
327
|
+
loadThread(mailboxUid, id).then((loaded) => {
|
|
328
|
+
if (generation !== generationRef.current)
|
|
329
|
+
return;
|
|
330
|
+
const current = loaded.messages.filter((message) => !removedRef.current.has(message.uid));
|
|
331
|
+
const adopted = adoptedOutgoing(mailboxUid, current);
|
|
332
|
+
const refocus = adopted.find((uid) => document.activeElement === headerRefs.current[uid]);
|
|
333
|
+
// Drawn first, and only then is the pending card forgotten: there is never a frame with neither it nor the real message.
|
|
334
|
+
flushSync(() => {
|
|
335
|
+
setMessages((previous) => mergeThread(previous, current));
|
|
336
|
+
setTruncated(loaded.truncated);
|
|
337
|
+
if (adopted.length > 0) {
|
|
338
|
+
setExpandedUids((previous) => new Set([...previous, ...adopted]));
|
|
339
|
+
}
|
|
340
|
+
if (refocus) {
|
|
341
|
+
setPendingFocusUid(refocus);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
settleOutgoing(mailboxUid, current);
|
|
345
|
+
},
|
|
346
|
+
// Quietly: the next live update reads it again, and what is shown is still right.
|
|
347
|
+
() => undefined);
|
|
348
|
+
}
|
|
349
|
+
// 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
|
|
350
|
+
// filed in Sent Items - whenever the shell announces a live update that touched one of this mailbox's folders (or does not say which).
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
if (live === seenLiveRef.current)
|
|
353
|
+
return;
|
|
354
|
+
seenLiveRef.current = live;
|
|
355
|
+
if (live.folderUids === null || folders.some((folder) => live.folderUids.has(folder.uid))) {
|
|
356
|
+
refreshThread();
|
|
357
|
+
}
|
|
358
|
+
}, [live]);
|
|
359
|
+
// 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.
|
|
360
|
+
const sentKey = outgoing
|
|
361
|
+
.filter((reply) => reply.state === "sent" && reply.mailboxUid === mailboxUid)
|
|
362
|
+
.map((reply) => reply.uid)
|
|
363
|
+
.join(",");
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
if (sentKey) {
|
|
366
|
+
refreshThread();
|
|
367
|
+
}
|
|
368
|
+
}, [sentKey]);
|
|
369
|
+
// 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
|
|
370
|
+
// that replies to nothing here - a new message, or a reply to another conversation - is not in this list, and never is.
|
|
371
|
+
const pendingCards = outgoing
|
|
372
|
+
.filter((reply) => reply.mailboxUid === mailboxUid && !messages.some((message) => message.uid === reply.uid) && belongsToThread(reply, messages))
|
|
373
|
+
.reverse();
|
|
374
|
+
// 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
|
|
375
|
+
// with it - once. A failed one, or one that was already there when the thread was opened, is left where it is.
|
|
376
|
+
const pendingKey = pendingCards.map((reply) => reply.uid).join(",");
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
const fresh = pendingCards.filter((reply) => reply.state === "sending" && !announcedRef.current.has(reply.uid));
|
|
379
|
+
if (fresh.length === 0)
|
|
380
|
+
return;
|
|
381
|
+
for (const reply of fresh) {
|
|
382
|
+
announcedRef.current.add(reply.uid);
|
|
383
|
+
}
|
|
384
|
+
setPendingFocusUid(fresh[0].uid);
|
|
385
|
+
}, [pendingKey]);
|
|
286
386
|
/** A newer copy of one of the thread's messages, kept here and handed to the list - with the copy it
|
|
287
387
|
* replaces where the caller was given one, so a conversation row can tell what actually changed. */
|
|
288
388
|
function patchMessage(updated, previous) {
|
|
@@ -291,6 +391,7 @@ export default function ConversationThreadPane({ conversation, mailboxUid, selec
|
|
|
291
391
|
}
|
|
292
392
|
/** A message that left the folder being listed - it leaves the thread too, as it left the list. */
|
|
293
393
|
function removeMessage(updated) {
|
|
394
|
+
removedRef.current.add(updated.uid);
|
|
294
395
|
setMessages((prev) => prev.filter((message) => message.uid !== updated.uid));
|
|
295
396
|
onMessageRemoved(updated);
|
|
296
397
|
}
|
|
@@ -329,26 +430,30 @@ export default function ConversationThreadPane({ conversation, mailboxUid, selec
|
|
|
329
430
|
: undefined
|
|
330
431
|
: error
|
|
331
432
|
? undefined
|
|
332
|
-
: `${messages.length} message${messages.length === 1 ? "" : "s"}`, children: truncated && !loading && !error && (_jsxs("p", { className: "text-xs text-text-muted mt-1", children: ["Only the oldest ", THREAD_MESSAGE_LIMIT, " messages of this conversation are shown here. The rest are still in the message list."] })) }), loading ? (_jsx(SkeletonCards, { messageCount: conversation.messageCount })) : error ? (_jsx("div", { className: "p-2 sm:p-3", children: _jsx(Alert, { children: error }) })) : (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
433
|
+
: `${messages.length + pendingCards.length} message${messages.length + pendingCards.length === 1 ? "" : "s"}`, children: truncated && !loading && !error && (_jsxs("p", { className: "text-xs text-text-muted mt-1", children: ["Only the oldest ", THREAD_MESSAGE_LIMIT, " messages of this conversation are shown here. The rest are still in the message list."] })) }), loading ? (_jsx(SkeletonCards, { messageCount: conversation.messageCount })) : error ? (_jsx("div", { className: "p-2 sm:p-3", children: _jsx(Alert, { children: error }) })) : (_jsxs("ul", { className: "flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3", children: [pendingCards.map((reply) => (_jsx("li", { ref: (node) => {
|
|
434
|
+
rowRefs.current[reply.uid] = node;
|
|
435
|
+
}, "data-outgoing": "true", children: _jsx(PendingMessageCard, { reply: reply, headerRef: (node) => {
|
|
436
|
+
headerRefs.current[reply.uid] = node;
|
|
437
|
+
} }) }, reply.uid))), messages.map((message) => {
|
|
438
|
+
const uid = message.uid;
|
|
439
|
+
const expanded = expandedUids.has(uid);
|
|
440
|
+
const bodyId = `thread-message-${uid}`;
|
|
441
|
+
const messageUnread = isUnread(message);
|
|
442
|
+
return (_jsx("li", { ref: (node) => {
|
|
443
|
+
rowRefs.current[uid] = node;
|
|
444
|
+
}, "data-unread": messageUnread ? "true" : undefined, children: expanded ? (_jsx(MessageDetailPane, { inThread: true, threadSubject: subject, threadHeader: {
|
|
445
|
+
bodyId,
|
|
446
|
+
unread: messageUnread,
|
|
447
|
+
onToggle: () => toggleExpanded(uid),
|
|
448
|
+
buttonRef: (node) => {
|
|
347
449
|
headerRefs.current[uid] = node;
|
|
348
|
-
},
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
450
|
+
},
|
|
451
|
+
}, footer: uid === footerUid, shortcuts: !!shortcuts && uid === keyboardUid, message: message, attachments: attachmentsByUid[uid] ?? [], isSentItems: folderTypeOf(message) === "sent_items", isOutbox: folderTypeOf(message) === "outbox", draftsFolderUid: folders.find((folder) => folder.type === "drafts")?.uid, folders: folders, onMoved: removeMessage, onFolderCreated: onFolderCreated, onRecalled: patchMessage, onReceiptHandled: patchMessage, onScheduledSendCanceled: removeMessage, onArchived: removeMessage, labels: labels, onLabelsChanged: patchMessage, onLabelCreated: onLabelCreated })) : (_jsxs(_Fragment, { children: [_jsx(CollapsedCard, { from: message.from, date: new Date(message.receivedDate).toLocaleString(), preview: message.bodyPreview || (message.encrypted ? _jsx(EncryptedPreview, {}) : ""), unread: messageUnread, senderClassName: senderClass(messageUnread), dateClassName: dateClass(messageUnread), buttonRef: (node) => {
|
|
452
|
+
headerRefs.current[uid] = node;
|
|
453
|
+
}, buttonProps: {
|
|
454
|
+
onClick: () => toggleExpanded(uid),
|
|
455
|
+
"aria-expanded": false,
|
|
456
|
+
"aria-controls": bodyId,
|
|
457
|
+
} }), _jsx("div", { id: bodyId, hidden: true })] })) }, uid));
|
|
458
|
+
})] }))] }));
|
|
354
459
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export { formatInviteWhen, isCalendarAttachment } from "./invite/inviteFormat.js";
|
|
3
|
+
export interface InviteCardProps {
|
|
4
|
+
/** The message whose calendar invitation to show, if it carries one. Nothing is drawn until the server says there is one. */
|
|
5
|
+
messageUid: string;
|
|
6
|
+
/** 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). */
|
|
7
|
+
headingLevel?: 2 | 3;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 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
|
|
11
|
+
* that answer it - Accept, Tentative, Decline (and Propose new time) for a request; "Add to calendar" for a published event; "Remove from
|
|
12
|
+
* calendar" for a cancellation; "Accept proposal" for an attendee's proposed new time. An attendee's answer to the reader's own invitation is
|
|
13
|
+
* one line ("Bob tentatively accepted."). Everything that follows an action (the meeting going onto the calendar or off it, the reply to the
|
|
14
|
+
* organizer) is the server's; this only asks for it and draws the invitation the server sends back.
|
|
15
|
+
*
|
|
16
|
+
* 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
|
|
17
|
+
* 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.
|
|
18
|
+
* An answer that arrives after the message changed or this was unmounted is dropped.
|
|
19
|
+
*/
|
|
20
|
+
export default function InviteCard({ messageUid, headingLevel }: InviteCardProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 { HiOutlineCalendarDays, HiOutlineCheck } from "react-icons/hi2";
|
|
8
|
+
import { acceptProposal, proposeNewTime, removeMessageInvite, respondToMessageInvite, } from "@rapidmx/react-shared/calendar/inviteApi.js";
|
|
9
|
+
import { guestPermissionsOf } from "@rapidmx/react-shared/calendar/calendarApi.js";
|
|
10
|
+
import { formatMailAddress } from "@rapidmx/react-shared/mail/mailAddress.js";
|
|
11
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
|
+
import EventDescriptionView from "../calendar/EventDescriptionView.js";
|
|
13
|
+
import RequestChangeForm from "../calendar/RequestChangeForm.js";
|
|
14
|
+
import { VISIBILITY_LABEL, describeGuestPermissions } from "../calendar/eventFormat.js";
|
|
15
|
+
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
16
|
+
import { calendarHref, conflictSummary, formatInviteWhen } from "./invite/inviteFormat.js";
|
|
17
|
+
import { useMessageInvite } from "./invite/inviteStore.js";
|
|
18
|
+
import ProposeTimeForm from "./invite/ProposeTimeForm.js";
|
|
19
|
+
export { formatInviteWhen, isCalendarAttachment } from "./invite/inviteFormat.js";
|
|
20
|
+
const HEADINGS = {
|
|
21
|
+
REQUEST: "Meeting invitation",
|
|
22
|
+
CANCEL: "Meeting canceled",
|
|
23
|
+
REPLY: "Meeting response",
|
|
24
|
+
COUNTER: "New time proposed",
|
|
25
|
+
};
|
|
26
|
+
/** The heading of a `COUNTER` that is a guest's request for a change (rather than a different time). */
|
|
27
|
+
const CHANGE_REQUEST_HEADING = "Change requested";
|
|
28
|
+
/** How an answer reads in "Bob accepted." (a REPLY) - a participant who has not answered has not answered. */
|
|
29
|
+
const REPLY_VERBS = {
|
|
30
|
+
accepted: "accepted",
|
|
31
|
+
tentative: "tentatively accepted",
|
|
32
|
+
declined: "declined",
|
|
33
|
+
};
|
|
34
|
+
/** The reader's own answer, as the line under the invitation says it. */
|
|
35
|
+
const ANSWER_TEXT = {
|
|
36
|
+
accepted: "You accepted this meeting.",
|
|
37
|
+
tentative: "You tentatively accepted this meeting.",
|
|
38
|
+
declined: "You declined this meeting. It is not on your calendar.",
|
|
39
|
+
};
|
|
40
|
+
const ANSWER_CLASS = {
|
|
41
|
+
accepted: "text-success",
|
|
42
|
+
tentative: "text-text",
|
|
43
|
+
declined: "text-danger",
|
|
44
|
+
};
|
|
45
|
+
const RESPONSES = [
|
|
46
|
+
{ response: "accepted", label: "Accept", failure: "Couldn't accept this meeting" },
|
|
47
|
+
{ response: "tentative", label: "Tentative", failure: "Couldn't respond tentatively to this meeting" },
|
|
48
|
+
{ response: "declined", label: "Decline", failure: "Couldn't decline this meeting" },
|
|
49
|
+
];
|
|
50
|
+
/** The most attendees that are listed by name; a longer list is only counted. */
|
|
51
|
+
const MAX_NAMED_ATTENDEES = 5;
|
|
52
|
+
function attendeeSummary(attendees) {
|
|
53
|
+
if (attendees.length === 0) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const count = `${attendees.length} ${attendees.length === 1 ? "attendee" : "attendees"}`;
|
|
57
|
+
return attendees.length <= MAX_NAMED_ATTENDEES ? `${count}: ${attendees.map((a) => a.displayName || a.address).join(", ")}` : count;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 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
|
|
61
|
+
* that answer it - Accept, Tentative, Decline (and Propose new time) for a request; "Add to calendar" for a published event; "Remove from
|
|
62
|
+
* calendar" for a cancellation; "Accept proposal" for an attendee's proposed new time. An attendee's answer to the reader's own invitation is
|
|
63
|
+
* one line ("Bob tentatively accepted."). Everything that follows an action (the meeting going onto the calendar or off it, the reply to the
|
|
64
|
+
* organizer) is the server's; this only asks for it and draws the invitation the server sends back.
|
|
65
|
+
*
|
|
66
|
+
* 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
|
|
67
|
+
* 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.
|
|
68
|
+
* An answer that arrives after the message changed or this was unmounted is dropped.
|
|
69
|
+
*/
|
|
70
|
+
export default function InviteCard({ messageUid, headingLevel = 2 }) {
|
|
71
|
+
const { invite, setInvite } = useMessageInvite(messageUid);
|
|
72
|
+
const [pending, setPending] = useState(null);
|
|
73
|
+
const [proposing, setProposing] = useState(false);
|
|
74
|
+
// Said once after a proposal went out: the invitation itself doesn't change.
|
|
75
|
+
const [proposed, setProposed] = useState(false);
|
|
76
|
+
// A guest's request for a change (or for guests to be added): the form is open, and once it went out.
|
|
77
|
+
const [requesting, setRequesting] = useState(false);
|
|
78
|
+
const [requested, setRequested] = useState(false);
|
|
79
|
+
const headingId = useId();
|
|
80
|
+
if (!invite) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/** 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. */
|
|
84
|
+
async function run(action, failure, call) {
|
|
85
|
+
setPending(action);
|
|
86
|
+
try {
|
|
87
|
+
setInvite(await call());
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
notifyApiError(err, failure);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
setPending(null);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function sendProposal(proposal) {
|
|
99
|
+
if (await run("propose", "Couldn't send the proposed time", () => proposeNewTime(messageUid, proposal))) {
|
|
100
|
+
setProposing(false);
|
|
101
|
+
setProposed(true);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const busy = pending !== null;
|
|
105
|
+
const Heading = `h${headingLevel}`;
|
|
106
|
+
const method = invite.method.toUpperCase();
|
|
107
|
+
const isReply = method === "REPLY";
|
|
108
|
+
const isCounter = method === "COUNTER";
|
|
109
|
+
// A `COUNTER` a guest sent to change the event (not just its time): whether the organizer's server applied it already.
|
|
110
|
+
const changeRequest = isCounter ? invite.changeRequest : undefined;
|
|
111
|
+
const permissions = guestPermissionsOf(invite.guestPermissions ?? {});
|
|
112
|
+
const hiddenList = !invite.isOrganizer && !permissions.guestsCanSeeGuestList;
|
|
113
|
+
const canRequest = !!invite.calendarEventUid && (!!invite.canRequestChange || !!invite.canRequestInvite);
|
|
114
|
+
const title = invite.summary?.trim() || "(no title)";
|
|
115
|
+
const when = formatInviteWhen(invite);
|
|
116
|
+
const attendees = hiddenList ? "The organizer has hidden the guest list" : attendeeSummary(invite.attendees);
|
|
117
|
+
const hasDescription = !!(invite.descriptionHtml?.trim() || invite.description?.trim());
|
|
118
|
+
const visibility = invite.visibility && invite.visibility !== "default" ? VISIBILITY_LABEL[invite.visibility] : undefined;
|
|
119
|
+
const sender = invite.reply ?? invite.attendees[0];
|
|
120
|
+
const senderName = sender ? sender.displayName || sender.address : "Someone";
|
|
121
|
+
const showResponses = invite.canRespond && !invite.isOrganizer;
|
|
122
|
+
const conflicts = !isReply && !isCounter && method !== "CANCEL" && invite.conflicts.length > 0 ? conflictSummary(invite.conflicts) : undefined;
|
|
123
|
+
const hasActions = showResponses ||
|
|
124
|
+
invite.canPropose ||
|
|
125
|
+
invite.canAcceptProposal ||
|
|
126
|
+
invite.canAdd ||
|
|
127
|
+
invite.canRemove ||
|
|
128
|
+
canRequest ||
|
|
129
|
+
(invite.onCalendar && !!invite.calendarEventUid);
|
|
130
|
+
return (_jsxs("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", children: [_jsxs(Heading, { id: headingId, className: "flex items-center gap-2 text-sm font-semibold", children: [_jsx(HiOutlineCalendarDays, { size: 16, "aria-hidden": "true", className: "shrink-0" }), changeRequest ? CHANGE_REQUEST_HEADING : (HEADINGS[method] ?? "Calendar event")] }), isReply && (_jsxs("p", { className: "break-words", children: [_jsx("span", { className: "font-semibold", children: senderName }), " ", (sender?.responseStatus && REPLY_VERBS[sender.responseStatus]) ?? "responded", "."] })), isCounter && !changeRequest && (_jsxs("p", { className: "break-words", children: [_jsx("span", { className: "font-semibold", children: senderName }), " proposed a new time", when ? _jsxs(_Fragment, { children: [": ", when] }) : "."] })), changeRequest && (_jsx("p", { className: "break-words", children: changeRequest.applied ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-semibold", children: senderName }), "\u2019s change was applied."] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-semibold", children: senderName }), " requested a change."] })) })), _jsx("p", { className: "font-medium break-words", children: title }), !isReply && (_jsxs("dl", { className: "grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-1", children: [when && (!isCounter || !!changeRequest) && (_jsxs(_Fragment, { children: [_jsx("dt", { className: "text-text-muted", children: "When" }), _jsxs("dd", { className: "break-words", children: [when, invite.recurring && _jsx("span", { className: "text-text-muted", children: " (Repeats)" })] })] })), invite.location && (_jsxs(_Fragment, { children: [_jsx("dt", { className: "text-text-muted", children: "Where" }), _jsx("dd", { className: "break-words", children: invite.location })] })), hasDescription && (!isCounter || !!changeRequest) && (_jsxs(_Fragment, { children: [_jsx("dt", { className: "text-text-muted", children: "Description" }), _jsx("dd", { className: "break-words", children: _jsx(EventDescriptionView, { html: invite.descriptionHtml, text: invite.description }) })] })), invite.organizer && !isCounter && (_jsxs(_Fragment, { children: [_jsx("dt", { className: "text-text-muted", children: "Organizer" }), _jsx("dd", { className: "break-words", children: formatMailAddress(invite.organizer) })] })), attendees && (!isCounter || !!changeRequest) && (_jsxs(_Fragment, { children: [_jsx("dt", { className: "text-text-muted", children: "Attendees" }), _jsx("dd", { className: "break-words", children: attendees })] })), visibility && !isCounter && (_jsxs(_Fragment, { children: [_jsx("dt", { className: "text-text-muted", children: "Visibility" }), _jsx("dd", { className: "break-words", children: visibility })] }))] })), isReply && when && _jsx("p", { className: "text-text-muted break-words", children: when }), conflicts && (_jsxs("p", { className: "text-danger break-words", children: [_jsx("span", { className: "font-medium", children: "Conflicts with:" }), " ", conflicts] })), invite.guestPermissions && !isReply && !isCounter && method !== "CANCEL" && (_jsx("p", { className: "text-xs text-text-muted break-words", children: describeGuestPermissions(permissions) })), invite.isOrganizer && !isReply && !isCounter && _jsx("p", { className: "text-text-muted", children: "You are the organizer of this meeting." }), method === "CANCEL" && _jsx("p", { className: "font-medium text-danger", children: "This meeting was canceled." }), invite.response && !invite.isOrganizer && !isReply && !isCounter && (_jsx("p", { role: "status", className: `font-medium ${ANSWER_CLASS[invite.response]}`, children: ANSWER_TEXT[invite.response] })), isCounter && invite.response === "accepted" && !changeRequest?.applied && (_jsx("p", { role: "status", className: "font-medium text-success", children: "You accepted the proposed time." })), proposed && (_jsx("p", { role: "status", className: "font-medium text-text", children: "Your proposed time was sent to the organizer." })), requested && (_jsx("p", { role: "status", className: "font-medium text-text", children: "Your change was sent to the organizer." })), invite.outdated && _jsx("p", { className: "text-xs text-text-muted", children: "A newer version of this meeting is already on your calendar." }), hasActions && (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [showResponses && (_jsx("div", { role: "group", "aria-label": "Respond to this meeting", "aria-busy": busy || undefined, className: "flex flex-wrap gap-2", children: RESPONSES.map(({ response, label, failure }) => {
|
|
131
|
+
const current = invite.response === response;
|
|
132
|
+
return (_jsxs(Button, { type: "button", variant: "secondary", className: ["!w-auto", current ? "!bg-primary/10 !border-primary disabled:!opacity-100" : ""].join(" "), "aria-pressed": current, "aria-busy": pending === response || undefined, loading: pending === response, disabled: busy || current, onClick: () => void run(response, failure, () => respondToMessageInvite(messageUid, response)), children: [current && _jsx(HiOutlineCheck, { size: 14, "aria-hidden": "true" }), label] }, response));
|
|
133
|
+
}) })), invite.canPropose && !proposing && (_jsx(Button, { type: "button", variant: "text", disabled: busy, onClick: () => setProposing(true), children: "Propose new time" })), canRequest && !requesting && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: busy, onClick: () => setRequesting(true), children: invite.canRequestChange ? "Request a change" : "Add guests" })), invite.canAcceptProposal && invite.response !== "accepted" && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-busy": pending === "acceptProposal" || undefined, loading: pending === "acceptProposal", disabled: busy, onClick: () => void run("acceptProposal", "Couldn't accept the proposed time", () => acceptProposal(messageUid)), children: "Accept proposal" })), invite.canAdd && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-busy": pending === "add" || undefined, loading: pending === "add", disabled: busy,
|
|
134
|
+
// A published event has no organizer to answer, so adding it is accepting it: the server sends no reply.
|
|
135
|
+
onClick: () => void run("add", "Couldn't add this event to your calendar", () => respondToMessageInvite(messageUid, "accepted")), children: "Add to calendar" })), invite.canRemove && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-busy": pending === "remove" || undefined, loading: pending === "remove", disabled: busy, onClick: () => void run("remove", "Couldn't remove this meeting from your calendar", () => removeMessageInvite(messageUid)), children: "Remove from calendar" })), invite.onCalendar && invite.calendarEventUid && (_jsx("a", { href: calendarHref(invite), className: "text-sm font-medium text-primary-dark hover:underline", children: "Open in Calendar" }))] })), requesting && (_jsx("div", { className: "rounded-md bg-surface p-3", children: _jsx(RequestChangeForm, { subject: {
|
|
136
|
+
uid: invite.calendarEventUid,
|
|
137
|
+
title: invite.summary ?? "",
|
|
138
|
+
location: invite.location,
|
|
139
|
+
startDate: invite.startDate,
|
|
140
|
+
endDate: invite.endDate,
|
|
141
|
+
allDay: invite.allDay,
|
|
142
|
+
recurring: invite.recurring,
|
|
143
|
+
description: invite.description,
|
|
144
|
+
descriptionHtml: invite.descriptionHtml,
|
|
145
|
+
guestAddresses: invite.attendees.map((attendee) => attendee.address),
|
|
146
|
+
}, canChange: !!invite.canRequestChange, canInvite: !!invite.canRequestInvite, onSent: () => {
|
|
147
|
+
setRequesting(false);
|
|
148
|
+
setRequested(true);
|
|
149
|
+
}, onCancel: () => setRequesting(false) }) })), proposing && (_jsx(ProposeTimeForm, { invite: invite, busy: pending === "propose", onSubmit: (proposal) => void sendProposal(proposal), onCancel: () => setProposing(false) }))] }));
|
|
150
|
+
}
|
|
@@ -47,6 +47,11 @@ export interface MailSelectionBarProps {
|
|
|
47
47
|
/** The keyboard acts on this selection (Delete, Ctrl+Q, Ctrl+U, Insert - registered by the page): Mark read, Mark unread, Flag and
|
|
48
48
|
* Delete name their shortcut in the tooltip and `aria-keyshortcuts`. */
|
|
49
49
|
shortcuts?: boolean;
|
|
50
|
+
/** Why Move to is unavailable, when the selection is spread over mailboxes and a folder can only belong to one (search results over several
|
|
51
|
+
* mailboxes). Shown as the button's tooltip. */
|
|
52
|
+
moveDisabledReason?: string;
|
|
53
|
+
/** The same for Apply label: a label belongs to one mailbox, and `labels` are one mailbox's. */
|
|
54
|
+
labelsDisabledReason?: string;
|
|
50
55
|
}
|
|
51
56
|
/**
|
|
52
57
|
* The header that replaces the list's own toolbar while select mode is on: how many rows are selected,
|
|
@@ -58,4 +63,4 @@ export interface MailSelectionBarProps {
|
|
|
58
63
|
* target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
|
|
59
64
|
* of actions doesn't shift around between folders.
|
|
60
65
|
*/
|
|
61
|
-
export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, shortcuts, }: MailSelectionBarProps): React.JSX.Element;
|
|
66
|
+
export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, shortcuts, moveDisabledReason, labelsDisabledReason, }: MailSelectionBarProps): React.JSX.Element;
|
|
@@ -22,7 +22,7 @@ function actionClassName() {
|
|
|
22
22
|
* target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
|
|
23
23
|
* of actions doesn't shift around between folders.
|
|
24
24
|
*/
|
|
25
|
-
export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, shortcuts, }) {
|
|
25
|
+
export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, shortcuts, moveDisabledReason, labelsDisabledReason, }) {
|
|
26
26
|
const env = useKeyEnvironment();
|
|
27
27
|
/** `title` and `aria-keyshortcuts` for an action the keyboard also does; `reason` (why it is disabled) wins the tooltip. */
|
|
28
28
|
const hint = (label, shortcut, reason) => shortcuts
|
|
@@ -49,7 +49,7 @@ export default function MailSelectionBar({ selected, listed, totals, onSelectAll
|
|
|
49
49
|
// partially applied, and is left exactly as it is unless the reader touches that row.
|
|
50
50
|
const appliedToAll = labels.filter((label) => !none && selected.every((m) => m.labelUids?.includes(label.uid)));
|
|
51
51
|
const appliedToSome = labels.filter((label) => selected.some((m) => m.labelUids?.includes(label.uid)) && !appliedToAll.includes(label));
|
|
52
|
-
return (_jsxs("div", { className: "border-b border-border", children: [_jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: mailboxUid, folders: folders, currentFolderUid: currentFolderUid, count: selected.length, onMove: onMoveTo, onFolderCreated: onFolderCreated }), _jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-surface-alt", children: [_jsxs("span", { "aria-live": "polite", className: "text-sm font-semibold text-text", children: [selectedCount, totals ? ` ${totals.noun}${selectedCount === 1 ? "" : "s"}` : "", " selected"] }), _jsx("button", { type: "button", onClick: onSelectAll, disabled: allSelected || listedCount === 0, className: actionClassName(), children: "Select all" }), _jsx("button", { type: "button", onClick: onClearSelection, disabled: selectedCount === 0, className: actionClassName(), children: "Clear" }), _jsx("button", { type: "button", onClick: onCancel, className: `${actionClassName()} ml-auto`, children: "Cancel" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-0.5 px-1.5 py-1", children: [_jsx("button", { type: "button", onClick: () => onSetRead(true), disabled: none || busy, className: actionClassName(), ...hint("Mark read", SHORTCUTS.mail.markRead), children: "Mark read" }), _jsx("button", { type: "button", onClick: () => onSetRead(false), disabled: none || busy, className: actionClassName(), ...hint("Mark unread", SHORTCUTS.mail.markUnread), children: "Mark unread" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(true), disabled: none || busy, className: actionClassName(), ...hint("Flag", SHORTCUTS.mail.flag), children: "Flag" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(false), disabled: none || busy, className: actionClassName(), children: "Unflag" }), _jsx("button", { type: "button", onClick: onArchive, disabled: none || busy || !!archiveReason, title: archiveReason, className: actionClassName(), children: "Archive" }), _jsx(LabelMenuButton, { "aria-label": "Apply label", label: "Apply label", className: "text-sm", labels: labels, mailboxUid: mailboxUid, onLabelCreated: onLabelCreated, applied: appliedToAll.map((label) => label.uid), partial: appliedToSome.map((label) => label.uid), onCommit: onApplyLabels, busy: busy, disabled: none || busy, note: selected.length === 1
|
|
52
|
+
return (_jsxs("div", { className: "border-b border-border", children: [_jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: mailboxUid, folders: folders, currentFolderUid: currentFolderUid, count: selected.length, onMove: onMoveTo, onFolderCreated: onFolderCreated }), _jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-surface-alt", children: [_jsxs("span", { "aria-live": "polite", className: "text-sm font-semibold text-text", children: [selectedCount, totals ? ` ${totals.noun}${selectedCount === 1 ? "" : "s"}` : "", " selected"] }), _jsx("button", { type: "button", onClick: onSelectAll, disabled: allSelected || listedCount === 0, className: actionClassName(), children: "Select all" }), _jsx("button", { type: "button", onClick: onClearSelection, disabled: selectedCount === 0, className: actionClassName(), children: "Clear" }), _jsx("button", { type: "button", onClick: onCancel, className: `${actionClassName()} ml-auto`, children: "Cancel" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-0.5 px-1.5 py-1", children: [_jsx("button", { type: "button", onClick: () => onSetRead(true), disabled: none || busy, className: actionClassName(), ...hint("Mark read", SHORTCUTS.mail.markRead), children: "Mark read" }), _jsx("button", { type: "button", onClick: () => onSetRead(false), disabled: none || busy, className: actionClassName(), ...hint("Mark unread", SHORTCUTS.mail.markUnread), children: "Mark unread" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(true), disabled: none || busy, className: actionClassName(), ...hint("Flag", SHORTCUTS.mail.flag), children: "Flag" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(false), disabled: none || busy, className: actionClassName(), children: "Unflag" }), _jsx("button", { type: "button", onClick: onArchive, disabled: none || busy || !!archiveReason, title: archiveReason, className: actionClassName(), children: "Archive" }), _jsx(LabelMenuButton, { "aria-label": "Apply label", label: "Apply label", className: "text-sm", labels: labels, mailboxUid: mailboxUid, onLabelCreated: onLabelCreated, applied: appliedToAll.map((label) => label.uid), partial: appliedToSome.map((label) => label.uid), onCommit: onApplyLabels, busy: busy, disabled: none || busy || !!labelsDisabledReason, title: labelsDisabledReason, note: selected.length === 1
|
|
53
53
|
? "Ticked labels are applied, unticked ones removed."
|
|
54
|
-
: "Ticked labels are applied to every selected message and unticked ones removed from all of them; a dash means only some have that label, and leaving it alone keeps it that way.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }), _jsx("button", { type: "button", onClick: () => setMovePrompt(true), disabled: none || busy, className: actionClassName(), children: "Move to" }), _jsx("button", { type: "button", onClick: onReportJunk, disabled: none || busy || !!junkReason, title: junkReason, className: actionClassName(), children: "Report junk" }), _jsx("button", { type: "button", onClick: onDelete, disabled: none || busy || !!deleteReason, className: actionClassName(), ...hint("Delete", SHORTCUTS.mail.delete, deleteReason), children: "Delete" })] })] }));
|
|
54
|
+
: "Ticked labels are applied to every selected message and unticked ones removed from all of them; a dash means only some have that label, and leaving it alone keeps it that way.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }), _jsx("button", { type: "button", onClick: () => setMovePrompt(true), disabled: none || busy || !!moveDisabledReason, title: moveDisabledReason, className: actionClassName(), children: "Move to" }), _jsx("button", { type: "button", onClick: onReportJunk, disabled: none || busy || !!junkReason, title: junkReason, className: actionClassName(), children: "Report junk" }), _jsx("button", { type: "button", onClick: onDelete, disabled: none || busy || !!deleteReason, className: actionClassName(), ...hint("Delete", SHORTCUTS.mail.delete, deleteReason), children: "Delete" })] })] }));
|
|
55
55
|
}
|
|
@@ -9,6 +9,9 @@ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
|
9
9
|
import { approveReceipt, archiveMessage, attachmentContentUrl, cancelScheduledSend, declineReceipt, getMailbox, getMessage, getMessageRawContent, moveMessage, recallMessage, setMessageLabels, } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
10
10
|
import { buildForwardQuote, buildReplyQuote, buildReplyRecipients, buildReplyThreading, forwardSubject, replySubject, } from "@rapidmx/react-shared/mail/compose/composeQuoting.js";
|
|
11
11
|
import MoveToFolderDialog from "./MoveToFolderDialog.js";
|
|
12
|
+
import InviteCard from "./InviteCard.js";
|
|
13
|
+
import { isCalendarAttachment } from "./invite/inviteFormat.js";
|
|
14
|
+
import { useMessageInvite } from "./invite/inviteStore.js";
|
|
12
15
|
import { getUnlockedKeys, subscribeKeySession } from "@rapidmx/react-shared/crypto/keySession.js";
|
|
13
16
|
import { extractAddresses } from "@rapidmx/react-shared/crypto/mime.js";
|
|
14
17
|
import { SignerKeyConflictError, signingKeyFingerprints, trustSigner } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
|
|
@@ -744,6 +747,14 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
|
|
|
744
747
|
const forwarded = message.flags.forwarded === true;
|
|
745
748
|
const cardUnread = threadHeader?.unread ?? false;
|
|
746
749
|
const showFooter = footer ?? !inThread;
|
|
750
|
+
// Only a message the server can read and that carries a calendar file is worth asking about an invitation. An encrypted message is out: the
|
|
751
|
+
// server holds only its ciphertext (the calendar file is inside it), so there is nothing for it to find. Drafts and Outbox hold messages
|
|
752
|
+
// that are being written or sent, not received invitations.
|
|
753
|
+
const mayHoldInvite = !message.encrypted && !inOutbox && message.folderUid !== draftsFolderUid && attachments.some(isCalendarAttachment);
|
|
754
|
+
// The calendar file of a message whose invitation card is drawn is the card, not a chip in the attachment list (a nameless "attachment" to click):
|
|
755
|
+
// it is listed again if the card could not be drawn (no invitation the server can read, or the lookup failed), so the file is never out of reach.
|
|
756
|
+
const { invite: shownInvite } = useMessageInvite(message.uid, mayHoldInvite);
|
|
757
|
+
const listedAttachments = shownInvite ? attachments.filter((attachment) => !isCalendarAttachment(attachment)) : attachments;
|
|
747
758
|
const senderRecipient = { displayName: senderName, address: senderAddress };
|
|
748
759
|
const sendingStatus = (_jsxs(_Fragment, { children: [sendInProgress && (_jsx("span", { className: "text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt", children: "Sending\u2026" })), isSentItems &&
|
|
749
760
|
(message.recallRequestedAt ? (_jsx("span", { className: "text-xs font-medium text-text-muted shrink-0 py-1 px-2.5 rounded-pill bg-surface-alt", children: "Recall requested" })) : (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => setConfirming(true), children: "Recall this message" }))), inOutbox && !sendInProgress && message.scheduledSendTime && (_jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [_jsxs("span", { className: "text-xs font-medium text-text-muted py-1 px-2.5 rounded-pill bg-surface-alt", children: ["Scheduled for ", new Date(message.scheduledSendTime).toLocaleString()] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: canceling, disabled: canceling, onClick: handleCancelScheduledSend, children: "Cancel" })] })), inOutbox && !sendInProgress && !message.scheduledSendTime && draftsFolderUid && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", loading: canceling, disabled: canceling, onClick: handleCancelScheduledSend, children: "Move to Drafts" }))] }));
|
|
@@ -768,8 +779,8 @@ function MessageDetailContent({ message: messageProp, attachments, backHref, isS
|
|
|
768
779
|
.filter((l) => !!l)
|
|
769
780
|
.map((l) => (_jsxs("span", { className: "inline-flex items-center gap-1.5 text-xs font-medium py-1 px-2.5 rounded-pill bg-surface-alt", children: [_jsx("span", { className: "w-2 h-2 rounded-full shrink-0", style: { backgroundColor: l.color ?? "#6366f1" } }), l.name] }, l.uid))) })), ["delivery", "read"]
|
|
770
781
|
.filter((type) => (type === "delivery" ? message.deliveryReceiptPending : message.readReceiptPending))
|
|
771
|
-
.map((type) => (_jsxs("div", { className: "flex flex-wrap items-center gap-2 py-2 px-3 rounded-sm bg-surface-alt text-sm", children: [_jsxs("span", { children: [senderLabel, " requested a ", type, " receipt for this message."] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: receiptBusy === type, disabled: receiptBusy !== null, onClick: () => handleReceipt(type, "approve"), children: "Send receipt" }), _jsx(Button, { type: "button", variant: "text", disabled: receiptBusy !== null, onClick: () => handleReceipt(type, "decline"), children: "Decline" })] }, type))), receiptError && _jsx(Alert, { children: receiptError }), security?.state === "signature_failed" && innerAttachments && innerAttachments.length > 0 && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: "These attachments come from a message whose signature couldn\u2019t be verified. Open them only if you trust the sender." })), innerAttachments
|
|
782
|
+
.map((type) => (_jsxs("div", { className: "flex flex-wrap items-center gap-2 py-2 px-3 rounded-sm bg-surface-alt text-sm", children: [_jsxs("span", { children: [senderLabel, " requested a ", type, " receipt for this message."] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: receiptBusy === type, disabled: receiptBusy !== null, onClick: () => handleReceipt(type, "approve"), children: "Send receipt" }), _jsx(Button, { type: "button", variant: "text", disabled: receiptBusy !== null, onClick: () => handleReceipt(type, "decline"), children: "Decline" })] }, type))), receiptError && _jsx(Alert, { children: receiptError }), mayHoldInvite && _jsx(InviteCard, { messageUid: message.uid, headingLevel: inThread ? 3 : 2 }), security?.state === "signature_failed" && innerAttachments && innerAttachments.length > 0 && (_jsx("p", { role: "status", className: "py-2 px-3 rounded-sm text-sm bg-warning/15 text-text", children: "These attachments come from a message whose signature couldn\u2019t be verified. Open them only if you trust the sender." })), innerAttachments
|
|
772
783
|
? innerAttachments.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-2", children: innerAttachments.map((attachment, index) => (_jsx("li", { children: _jsx("button", { type: "button", onClick: () => downloadMimeAttachment(attachment), className: "text-xs font-medium py-1 px-2.5 rounded-pill bg-surface-alt text-text-muted hover:text-primary-dark", children: attachment.filename ?? `Unnamed ${attachment.contentType} attachment` }) }, `${index}:${attachment.filename ?? ""}`))) }))
|
|
773
|
-
:
|
|
784
|
+
: listedAttachments.length > 0 && (_jsx("ul", { className: "flex flex-wrap gap-2", children: listedAttachments.map((attachment) => (_jsx("li", { children: _jsxs("a", { href: attachmentContentUrl(attachment.uid), className: "text-xs font-medium py-1 px-2.5 rounded-pill bg-surface-alt text-text-muted hover:text-primary-dark", children: [attachment.filename, " (", formatBytes(attachment.sizeBytes), ")"] }) }, attachment.uid))) })), _jsx("div", { ref: bodyRegionRef, tabIndex: -1, className: "outline-none", children: message.encrypted && security === null ? (_jsx(BodySkeleton, {})) : security?.decryptError !== undefined ? (_jsx(EncryptedBody, { locked: lockedRef.current, reason: security.decryptError, unlocking: unlocking, onUnlock: () => void handleUnlockToView() })) : (_jsx(MessageBody, { messageUid: message.uid, messageVersion: message.version, title: bodyTitle, content: bodyContent, attachments: attachments, inlineParts: innerAttachments, original: viewOriginal, onAdaptable: setAdaptable })) })] }), showFooter && (_jsxs("div", { className: "flex flex-wrap gap-2 px-4 py-3 border-t border-border print:hidden", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Reply to this message", disabled: preparingCompose, onClick: () => void handleReplyOrForward("reply"), children: "Reply" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Reply all to this message", disabled: preparingCompose, onClick: () => void handleReplyOrForward("replyAll"), children: "Reply All" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-label": "Forward this message", disabled: preparingCompose, onClick: () => void handleReplyOrForward("forward"), children: "Forward" })] }))] }));
|
|
774
785
|
return (_jsxs("div", { className: inThread ? "min-w-0" : "flex-1 min-w-0 min-h-0 flex flex-col h-full", children: [inThread ? (card) : (_jsxs(_Fragment, { children: [backHref && (_jsx("a", { href: backHref, className: "text-sm text-primary-dark hover:underline block px-4 pt-3", children: "\u2190 Back to messages" })), _jsx(SubjectCard, { subject: shownSubject }), _jsx("div", { className: "flex-1 min-h-0 overflow-y-auto p-2 sm:p-3 flex flex-col gap-3", children: card })] })), _jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: message.mailboxUid, folders: folders ?? [], currentFolderUid: message.folderUid, onMove: handleMove, onFolderCreated: onFolderCreated }), _jsxs(Modal, { open: confirming, onClose: () => setConfirming(false), title: "Recall this message?", children: [_jsx("p", { className: "text-sm text-text-muted mb-4", children: "This asks every original recipient's mail system to delete their copy, but only if it's still unread there \u2014 there's no way to guarantee it, and no confirmation once it either succeeds or fails. Recipients who already read the message will keep it." }), error && _jsx(Alert, { children: error }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", loading: recalling, disabled: recalling, onClick: handleRecall, className: "!w-auto", children: "Recall message" }), _jsx(Button, { type: "button", variant: "secondary", disabled: recalling, onClick: () => setConfirming(false), className: "!w-auto", children: "Cancel" })] })] }), _jsx(Modal, { open: trustConfirmOpen && !!trustableCertificate, onClose: () => !trusting && setTrustConfirmOpen(false), title: "Trust this signer?", children: _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsxs("p", { children: ["Mail from ", _jsx("span", { className: "font-medium", children: senderAddress }), " signed with this certificate will show as verified."] }), _jsxs("dl", { className: "flex flex-col gap-1", children: [_jsx("dt", { className: "text-xs text-text-muted", children: "Certificate issued to" }), _jsx("dd", { children: security?.signerEmails?.length ? security.signerEmails.join(", ") : "No email address" }), _jsx("dt", { className: "text-xs text-text-muted", children: "Fingerprint" }), _jsx("dd", { className: "font-mono text-xs break-all", children: security?.signerFingerprint ? formatFingerprint(security.signerFingerprint) : "Unknown" })] }), _jsx("p", { className: "text-text-muted", children: "Anyone can create a certificate naming any address. Before trusting it, confirm this fingerprint with the sender through another channel, such as a phone call." }), trustError && _jsx(Alert, { children: trustError }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "button", className: "!w-auto", loading: trusting, disabled: trusting, onClick: () => handleTrustSigner(senderAddress, trustableCertificate), children: "Trust" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: trusting, onClick: () => setTrustConfirmOpen(false), children: "Cancel" })] })] }) })] }));
|
|
775
786
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React, { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
export interface SwipeRowProps extends HTMLAttributes<HTMLElement> {
|
|
3
|
+
/** The element the row is: a list item, or the row's own `div` inside one. */
|
|
4
|
+
as?: "li" | "div";
|
|
5
|
+
/** Swipes are only followed while this is `true`: the phone layout, and not while rows are being selected. */
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
/** Right to left. Resolves whether it went through; `false` brings the row back, anything else leaves it gone (its list removes it). */
|
|
8
|
+
onArchive: () => Promise<boolean>;
|
|
9
|
+
/** Left to right. The row comes back at once, and this asks where to (a prompt for the folder). */
|
|
10
|
+
onMove: () => void;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A row of a list of mail that slides with a finger: right to left it takes the row away to Archive (a green panel with the
|
|
15
|
+
* archive icon follows the row in), left to right it comes back and asks for a folder to move it to (a blue panel with a
|
|
16
|
+
* folder). The panels are children of the row placed just outside its edges, so they travel with it and fill the gap it leaves
|
|
17
|
+
* without the row's own markup, or the list's, changing; the list has to clip them (`overflow-x-clip`).
|
|
18
|
+
*
|
|
19
|
+
* Only a swipe that starts out horizontal counts (see `useSwipe()`), so scrolling the list is untouched, and it is an addition
|
|
20
|
+
* to, never the only way of, archiving and moving: the selection bar and the reading pane do the same.
|
|
21
|
+
*/
|
|
22
|
+
export default function SwipeRow({ as: Tag, enabled, onArchive, onMove, children, className, style, ...rest }: SwipeRowProps): React.JSX.Element;
|