@rapidmx/web-client 0.14.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +373 -373
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -480
- package/apps/admin/domains/[uid].tsx +164 -163
- package/apps/admin/escrow-scopes/[uid].tsx +348 -348
- package/apps/admin/index.tsx +127 -124
- package/apps/admin/mailboxes/[uid].tsx +269 -224
- package/apps/admin/mailboxes/new/index.tsx +28 -28
- package/apps/admin/plugins/index.tsx +15 -15
- package/apps/admin/retention-policy/index.tsx +39 -39
- package/apps/admin/signing-certificates/index.tsx +343 -343
- package/apps/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/shared/appearance/AppearanceForm.tsx +9 -1
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/calendar/bookingPlugin.ts +100 -0
- package/apps/shared/calendar/calendarLiveUpdates.ts +27 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +378 -346
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
- package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +53 -4
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +64 -10
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
- package/apps/shared/components/admin/setup/SetupWizard.tsx +444 -444
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/AppointmentQuickForm.tsx +327 -0
- package/apps/shared/components/calendar/DescriptionEditor.tsx +274 -0
- package/apps/shared/components/calendar/EventDescriptionView.tsx +70 -0
- package/apps/shared/components/calendar/EventDetails.tsx +401 -0
- package/apps/shared/components/calendar/EventEditor.tsx +635 -0
- package/apps/shared/components/calendar/EventExpandedForm.tsx +210 -0
- package/apps/shared/components/calendar/EventFormParts.tsx +591 -0
- package/apps/shared/components/calendar/EventModal.tsx +206 -946
- package/apps/shared/components/calendar/EventQuickForm.tsx +116 -0
- package/apps/shared/components/calendar/EventShell.tsx +202 -0
- package/apps/shared/components/calendar/FindATime.tsx +379 -0
- package/apps/shared/components/calendar/GuestInput.tsx +100 -0
- package/apps/shared/components/calendar/LazyDescriptionEditor.tsx +70 -0
- package/apps/shared/components/calendar/MonthView.tsx +185 -173
- package/apps/shared/components/calendar/QuickCreateFaces.tsx +87 -0
- package/apps/shared/components/calendar/QuickCreateTabs.tsx +81 -0
- package/apps/shared/components/calendar/QuickFaceFrame.tsx +81 -0
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -222
- package/apps/shared/components/calendar/RequestChangeForm.tsx +240 -0
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -143
- package/apps/shared/components/calendar/TaskQuickForm.tsx +289 -0
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -244
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/calendar/eventDialogFields.ts +86 -0
- package/apps/shared/components/calendar/eventForm.ts +108 -0
- package/apps/shared/components/calendar/eventFormat.ts +210 -0
- package/apps/shared/components/calendar/findTimeGrid.ts +90 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
- package/apps/shared/components/calendar/swipeNavigation.ts +14 -0
- package/apps/shared/components/contacts/ContactForm.tsx +383 -383
- package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
- package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +217 -216
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +480 -480
- package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
- package/apps/shared/components/layout/UserMenu.tsx +439 -372
- package/apps/shared/components/mail/ConversationList.tsx +323 -271
- package/apps/shared/components/mail/ConversationThreadPane.tsx +130 -5
- package/apps/shared/components/mail/InviteCard.tsx +387 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +236 -227
- package/apps/shared/components/mail/MessageDetailPane.tsx +1559 -1537
- package/apps/shared/components/mail/SwipeRow.tsx +120 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1673
- package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
- package/apps/shared/components/mail/invite/InviteRowChip.tsx +104 -0
- package/apps/shared/components/mail/invite/InviteRsvpPopover.tsx +271 -0
- package/apps/shared/components/mail/invite/InviteTimeline.tsx +200 -0
- package/apps/shared/components/mail/invite/ProposeTimeForm.tsx +109 -0
- package/apps/shared/components/mail/invite/inviteFormat.ts +110 -0
- package/apps/shared/components/mail/invite/inviteStore.ts +114 -0
- package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +546 -490
- package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
- package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -125
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
- package/apps/shared/components/settings/layout/SettingsShell.tsx +255 -253
- package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
- package/apps/shared/components/tasks/layout/TasksShell.tsx +218 -217
- package/apps/shared/gestures/useEnterSlide.ts +54 -0
- package/apps/shared/gestures/useSwipe.ts +114 -0
- package/apps/shared/gestures/useSwipeSlide.ts +113 -0
- package/apps/shared/gestures/useWheelPaging.ts +107 -0
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
- package/apps/shared/keyboard/dispatch.ts +124 -124
- package/apps/shared/keyboard/format.ts +89 -89
- package/apps/shared/keyboard/keymap.ts +114 -114
- package/apps/shared/keyboard/registry.ts +65 -65
- package/apps/shared/keyboard/targets.ts +79 -79
- package/apps/shared/mail/folderCounts.ts +17 -0
- package/apps/shared/mail/folderTree.ts +143 -143
- package/apps/shared/mail/listAllPages.ts +39 -39
- package/apps/shared/mail/newMailNotifications.ts +3 -15
- package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
- package/apps/shared/mail/outbox/sendJob.ts +19 -1
- package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
- package/apps/shared/mail/primaryMailbox.ts +71 -0
- package/apps/shared/mail/useCollapsedSections.ts +146 -0
- package/apps/shared/mail/useMailConnection.ts +205 -203
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
- package/apps/shared/mail/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +2 -2
- package/apps/shared/navigation/AppRouter.tsx +300 -300
- package/apps/shared/navigation/routerContext.tsx +83 -83
- package/apps/shared/notifications/preferences.ts +36 -0
- package/apps/shared/notifications/store.ts +9 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
- package/apps/shared/search/crossMailboxSearch.ts +169 -0
- package/apps/shared/search/localIndexBuilder.ts +481 -481
- package/apps/shared/signing/enrollmentStorage.ts +33 -33
- package/apps/shared/signing/enrollmentTracker.ts +385 -385
- package/apps/shared/signing/enrollmentView.ts +251 -251
- package/apps/shared/signing/useNow.ts +19 -19
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
- package/apps/shared/styles/app.css +396 -386
- package/apps/www/_routes.ts +1 -0
- package/apps/www/calendar/index.tsx +578 -469
- package/apps/www/contacts/[uid].tsx +109 -109
- package/apps/www/contacts/index.tsx +6 -0
- package/apps/www/index.tsx +2847 -2332
- package/apps/www/messages/[uid].tsx +135 -108
- package/apps/www/settings/auto-reply/index.tsx +133 -133
- package/apps/www/settings/encryption/index.tsx +1287 -1287
- package/apps/www/settings/filters/[uid].tsx +176 -176
- package/apps/www/settings/filters/index.tsx +102 -102
- package/apps/www/settings/filters/new/index.tsx +140 -140
- package/apps/www/settings/labels/index.tsx +204 -204
- package/apps/www/settings/privacy/index.tsx +492 -492
- package/apps/www/settings/profile/index.tsx +248 -0
- package/apps/www/settings/read-receipts/index.tsx +147 -147
- package/apps/www/settings/sharing/index.tsx +256 -256
- package/apps/www/settings/signatures/[uid].tsx +172 -172
- package/apps/www/settings/signatures/index.tsx +88 -88
- package/apps/www/settings/signatures/new/index.tsx +135 -135
- package/apps/www/tasks/index.tsx +649 -649
- package/dist/apps/admin/data-requests/index.js +1 -1
- package/dist/apps/admin/domains/[uid].js +4 -2
- package/dist/apps/admin/index.js +2 -1
- package/dist/apps/admin/mailboxes/[uid].js +12 -2
- package/dist/apps/shared/appearance/AppearanceForm.js +9 -1
- package/dist/apps/shared/calendar/bookingPlugin.d.ts +50 -0
- package/dist/apps/shared/calendar/bookingPlugin.js +57 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -0
- package/dist/apps/shared/calendar/calendarLiveUpdates.js +23 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +7 -3
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +28 -4
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +40 -6
- package/dist/apps/shared/components/calendar/AppointmentQuickForm.d.ts +47 -0
- package/dist/apps/shared/components/calendar/AppointmentQuickForm.js +135 -0
- package/dist/apps/shared/components/calendar/DescriptionEditor.d.ts +26 -0
- package/dist/apps/shared/components/calendar/DescriptionEditor.js +192 -0
- package/dist/apps/shared/components/calendar/EventDescriptionView.d.ts +14 -0
- package/dist/apps/shared/components/calendar/EventDescriptionView.js +39 -0
- package/dist/apps/shared/components/calendar/EventDetails.d.ts +33 -0
- package/dist/apps/shared/components/calendar/EventDetails.js +137 -0
- package/dist/apps/shared/components/calendar/EventEditor.d.ts +59 -0
- package/dist/apps/shared/components/calendar/EventEditor.js +544 -0
- package/dist/apps/shared/components/calendar/EventExpandedForm.d.ts +14 -0
- package/dist/apps/shared/components/calendar/EventExpandedForm.js +44 -0
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +70 -0
- package/dist/apps/shared/components/calendar/EventFormParts.js +191 -0
- package/dist/apps/shared/components/calendar/EventModal.d.ts +26 -13
- package/dist/apps/shared/components/calendar/EventModal.js +57 -459
- package/dist/apps/shared/components/calendar/EventQuickForm.d.ts +18 -0
- package/dist/apps/shared/components/calendar/EventQuickForm.js +25 -0
- package/dist/apps/shared/components/calendar/EventShell.d.ts +64 -0
- package/dist/apps/shared/components/calendar/EventShell.js +134 -0
- package/dist/apps/shared/components/calendar/FindATime.d.ts +18 -0
- package/dist/apps/shared/components/calendar/FindATime.js +190 -0
- package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
- package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
- package/dist/apps/shared/components/calendar/LazyDescriptionEditor.d.ts +8 -0
- package/dist/apps/shared/components/calendar/LazyDescriptionEditor.js +44 -0
- package/dist/apps/shared/components/calendar/MonthView.d.ts +5 -1
- package/dist/apps/shared/components/calendar/MonthView.js +11 -5
- package/dist/apps/shared/components/calendar/QuickCreateFaces.d.ts +30 -0
- package/dist/apps/shared/components/calendar/QuickCreateFaces.js +44 -0
- package/dist/apps/shared/components/calendar/QuickCreateTabs.d.ts +24 -0
- package/dist/apps/shared/components/calendar/QuickCreateTabs.js +46 -0
- package/dist/apps/shared/components/calendar/QuickFaceFrame.d.ts +25 -0
- package/dist/apps/shared/components/calendar/QuickFaceFrame.js +19 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +2 -2
- package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +36 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +127 -0
- package/dist/apps/shared/components/calendar/SplitDayView.d.ts +3 -2
- package/dist/apps/shared/components/calendar/SplitDayView.js +3 -2
- package/dist/apps/shared/components/calendar/TaskQuickForm.d.ts +40 -0
- package/dist/apps/shared/components/calendar/TaskQuickForm.js +101 -0
- package/dist/apps/shared/components/calendar/TimeGridView.d.ts +4 -2
- package/dist/apps/shared/components/calendar/TimeGridView.js +3 -2
- package/dist/apps/shared/components/calendar/eventDialogFields.d.ts +28 -0
- package/dist/apps/shared/components/calendar/eventDialogFields.js +59 -0
- package/dist/apps/shared/components/calendar/eventForm.d.ts +98 -0
- package/dist/apps/shared/components/calendar/eventForm.js +1 -0
- package/dist/apps/shared/components/calendar/eventFormat.d.ts +80 -0
- package/dist/apps/shared/components/calendar/eventFormat.js +190 -0
- package/dist/apps/shared/components/calendar/findTimeGrid.d.ts +30 -0
- package/dist/apps/shared/components/calendar/findTimeGrid.js +83 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -2
- package/dist/apps/shared/components/calendar/swipeNavigation.d.ts +9 -0
- package/dist/apps/shared/components/calendar/swipeNavigation.js +8 -0
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -3
- package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
- package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +14 -9
- package/dist/apps/shared/components/layout/UserMenu.js +74 -26
- package/dist/apps/shared/components/mail/ConversationList.d.ts +12 -1
- package/dist/apps/shared/components/mail/ConversationList.js +21 -4
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
- package/dist/apps/shared/components/mail/InviteCard.d.ts +20 -0
- package/dist/apps/shared/components/mail/InviteCard.js +150 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -1
- package/dist/apps/shared/components/mail/MailSelectionBar.js +3 -3
- package/dist/apps/shared/components/mail/MessageDetailPane.js +13 -2
- package/dist/apps/shared/components/mail/SwipeRow.d.ts +22 -0
- package/dist/apps/shared/components/mail/SwipeRow.js +71 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +16 -7
- package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
- package/dist/apps/shared/components/mail/invite/InviteRowChip.d.ts +23 -0
- package/dist/apps/shared/components/mail/invite/InviteRowChip.js +46 -0
- package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.d.ts +20 -0
- package/dist/apps/shared/components/mail/invite/InviteRsvpPopover.js +141 -0
- package/dist/apps/shared/components/mail/invite/InviteTimeline.d.ts +27 -0
- package/dist/apps/shared/components/mail/invite/InviteTimeline.js +120 -0
- package/dist/apps/shared/components/mail/invite/ProposeTimeForm.d.ts +17 -0
- package/dist/apps/shared/components/mail/invite/ProposeTimeForm.js +45 -0
- package/dist/apps/shared/components/mail/invite/inviteFormat.d.ts +28 -0
- package/dist/apps/shared/components/mail/invite/inviteFormat.js +94 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.d.ts +28 -0
- package/dist/apps/shared/components/mail/invite/inviteStore.js +95 -0
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +10 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +76 -46
- package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
- package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +7 -5
- package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +4 -3
- package/dist/apps/shared/gestures/useEnterSlide.d.ts +14 -0
- package/dist/apps/shared/gestures/useEnterSlide.js +47 -0
- package/dist/apps/shared/gestures/useSwipe.d.ts +45 -0
- package/dist/apps/shared/gestures/useSwipe.js +90 -0
- package/dist/apps/shared/gestures/useSwipeSlide.d.ts +40 -0
- package/dist/apps/shared/gestures/useSwipeSlide.js +95 -0
- package/dist/apps/shared/gestures/useWheelPaging.d.ts +32 -0
- package/dist/apps/shared/gestures/useWheelPaging.js +85 -0
- package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
- package/dist/apps/shared/mail/folderCounts.js +12 -0
- package/dist/apps/shared/mail/newMailNotifications.d.ts +3 -8
- package/dist/apps/shared/mail/newMailNotifications.js +3 -12
- package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
- package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
- package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
- package/dist/apps/shared/mail/primaryMailbox.js +61 -0
- package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
- package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
- package/dist/apps/shared/mail/useMailConnection.js +3 -1
- package/dist/apps/shared/mail/useNewMailNotifications.js +3 -2
- package/dist/apps/shared/notifications/preferences.d.ts +13 -0
- package/dist/apps/shared/notifications/preferences.js +36 -0
- package/dist/apps/shared/notifications/store.d.ts +1 -0
- package/dist/apps/shared/notifications/store.js +7 -23
- package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
- package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
- package/dist/apps/shared/styles/app.css +396 -386
- package/dist/apps/www/_routes.js +1 -0
- package/dist/apps/www/calendar/index.js +78 -7
- package/dist/apps/www/contacts/index.js +3 -1
- package/dist/apps/www/index.js +548 -210
- package/dist/apps/www/messages/[uid].js +13 -5
- package/dist/apps/www/settings/profile/index.d.ts +4 -0
- package/dist/apps/www/settings/profile/index.js +124 -0
- package/package.json +2 -2
|
@@ -1,217 +1,218 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
|
6
|
-
import { HiOutlineBars3 } from "react-icons/hi2";
|
|
7
|
-
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
-
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
9
|
-
import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
10
|
-
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
-
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
12
|
-
import AppShell, { AppShellProps } from "../../layout/AppShell.js";
|
|
13
|
-
import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
|
|
14
|
-
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
mailboxUid
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const [
|
|
57
|
-
const [
|
|
58
|
-
const [
|
|
59
|
-
const [
|
|
60
|
-
const [
|
|
61
|
-
const [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
mailboxes
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
{mb.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { HiOutlineBars3 } from "react-icons/hi2";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
9
|
+
import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
10
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
+
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
12
|
+
import AppShell, { AppShellProps } from "../../layout/AppShell.js";
|
|
13
|
+
import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
|
|
14
|
+
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
15
|
+
import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
16
|
+
|
|
17
|
+
export type TasksShellProps = Omit<AppShellProps, "active">;
|
|
18
|
+
|
|
19
|
+
export interface TasksShellContextValue {
|
|
20
|
+
/** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox). */
|
|
21
|
+
mailboxUid?: string;
|
|
22
|
+
/** The selected mailbox's single `tasks`-type folder. */
|
|
23
|
+
folderUid?: string;
|
|
24
|
+
mailboxes: Mailbox[];
|
|
25
|
+
/** The caller's own uid — exposed here (rather than only as `AppShell`'s prop) so the page content
|
|
26
|
+
* can compute "assigned to me" without needing it threaded through separately. */
|
|
27
|
+
userUid?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TasksShellContext = createContext<TasksShellContextValue>({ mailboxes: [] });
|
|
31
|
+
|
|
32
|
+
/** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
|
|
33
|
+
export function useTasksShell(): TasksShellContextValue {
|
|
34
|
+
return useContext(TasksShellContext);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Status = "checking" | "error" | "ready";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The Tasks app's shell — structurally identical to `ContactsShell` (a mailbox has exactly one
|
|
41
|
+
* well-known `tasks` folder, guaranteed to exist by `BaseMailboxRoute.create()`'s eager provisioning
|
|
42
|
+
* in `@rapidmx/restapi`), just bound to a different folder type. Kept as its own small component
|
|
43
|
+
* (rather than a single generic "SingleFolderShell" parameterized by type) so each app's sidebar can
|
|
44
|
+
* grow its own app-specific controls later without threading extra props through a shared one.
|
|
45
|
+
*/
|
|
46
|
+
export default function TasksShell({
|
|
47
|
+
userUid,
|
|
48
|
+
authServerUrl,
|
|
49
|
+
impersonating,
|
|
50
|
+
impersonationBaseUrl,
|
|
51
|
+
trusted,
|
|
52
|
+
trustedRoles,
|
|
53
|
+
pluginNav,
|
|
54
|
+
children,
|
|
55
|
+
}: PropsWithChildren<TasksShellProps>) {
|
|
56
|
+
const [status, setStatus] = useState<Status>("checking");
|
|
57
|
+
const [error, setError] = useState<string | null>(null);
|
|
58
|
+
const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
|
|
59
|
+
const [folders, setFolders] = useState<Folder[]>([]);
|
|
60
|
+
const [folderError, setFolderError] = useState<string | null>(null);
|
|
61
|
+
const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
|
|
62
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
63
|
+
|
|
64
|
+
// Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
|
|
65
|
+
// effect, not during render, so the server render and the hydrating render agree.
|
|
66
|
+
const search = useLocationSearch();
|
|
67
|
+
const navigate = useNavigate();
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
|
|
70
|
+
}, [search]);
|
|
71
|
+
// Choosing a folder (or mailbox) changes the URL without a page load now, so the drawer that held the choice - which used to go
|
|
72
|
+
// with the page - is closed here.
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setDrawerOpen(false);
|
|
75
|
+
}, [search]);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!userUid) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
listMailboxes({ limit: 100 })
|
|
82
|
+
.then((result) => {
|
|
83
|
+
setMailboxes(orderMailboxes(result, userUid));
|
|
84
|
+
setStatus("ready");
|
|
85
|
+
})
|
|
86
|
+
.catch((err) => {
|
|
87
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
|
|
88
|
+
setStatus("error");
|
|
89
|
+
});
|
|
90
|
+
}, [userUid]);
|
|
91
|
+
|
|
92
|
+
const mailboxUid: string | undefined =
|
|
93
|
+
(requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
|
|
94
|
+
primaryMailboxUid(mailboxes, userUid);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!mailboxUid) {
|
|
98
|
+
setFolders([]);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
setFolderError(null);
|
|
102
|
+
listFolders(mailboxUid)
|
|
103
|
+
.then(setFolders)
|
|
104
|
+
.catch((err) => setFolderError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox's tasks folder."));
|
|
105
|
+
}, [mailboxUid]);
|
|
106
|
+
|
|
107
|
+
const folderUid: string | undefined = folders.find((f) => f.type === "tasks")?.uid;
|
|
108
|
+
|
|
109
|
+
const contextValue = useMemo<TasksShellContextValue>(
|
|
110
|
+
() => ({ mailboxUid, folderUid, mailboxes, userUid }),
|
|
111
|
+
[mailboxUid, folderUid, mailboxes, userUid],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
|
|
115
|
+
// for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
|
|
116
|
+
if (userUid && status === "ready" && !mailboxUid) {
|
|
117
|
+
return <MailboxProvisioning />;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let inner: ReactNode = null;
|
|
121
|
+
if (userUid && status === "checking") {
|
|
122
|
+
// Renders immediately (no network round trip needed) so switching into Tasks never shows a
|
|
123
|
+
// blank pane while `listMailboxes()` is in flight — the real sidebar (My Day/Important/Planned/
|
|
124
|
+
// lists) lives in `apps/www/tasks/index.tsx`'s own content, so this mimics its rough shape rather
|
|
125
|
+
// than the (minimal, mailbox-switcher-only) shape of this shell's own sidebar.
|
|
126
|
+
inner = (
|
|
127
|
+
<aside className="w-56 shrink-0 bg-surface border-r border-border flex flex-col p-3 gap-4">
|
|
128
|
+
<Skeleton height="h-9" />
|
|
129
|
+
<SkeletonList count={5} />
|
|
130
|
+
</aside>
|
|
131
|
+
);
|
|
132
|
+
} else if (userUid && status === "error") {
|
|
133
|
+
inner = (
|
|
134
|
+
<div className="flex-1 flex items-center justify-center p-8">
|
|
135
|
+
<div className="w-full max-w-md">
|
|
136
|
+
<Alert>{error}</Alert>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
} else if (userUid && status === "ready") {
|
|
141
|
+
// A function, not a plain JSX constant — see MailShell/ContactsShell/CalendarShell's identical
|
|
142
|
+
// comment: it's rendered twice (desktop `<aside>` + mobile `Drawer`), possibly simultaneously
|
|
143
|
+
// mounted, so the `<select>`'s `id`/its `<label>`'s `htmlFor` need a distinct value per instance.
|
|
144
|
+
const sidebarContent = (idPrefix: string) => (
|
|
145
|
+
<>
|
|
146
|
+
{mailboxes.length > 1 && (
|
|
147
|
+
<div>
|
|
148
|
+
<label
|
|
149
|
+
className="block text-xs font-bold uppercase tracking-wide text-text-muted mb-1"
|
|
150
|
+
htmlFor={`${idPrefix}-tasks-mailbox-switcher`}
|
|
151
|
+
>
|
|
152
|
+
Mailbox
|
|
153
|
+
</label>
|
|
154
|
+
<select
|
|
155
|
+
id={`${idPrefix}-tasks-mailbox-switcher`}
|
|
156
|
+
className="w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface"
|
|
157
|
+
value={mailboxUid}
|
|
158
|
+
onChange={(e) => {
|
|
159
|
+
navigate(`/tasks?mailboxUid=${encodeURIComponent(e.target.value)}`);
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{mailboxes.map((mb) => (
|
|
163
|
+
<option key={mb.uid} value={mb.uid}>
|
|
164
|
+
{mb.displayName}
|
|
165
|
+
{mb.ownerUserUid ? "" : " (shared)"}
|
|
166
|
+
</option>
|
|
167
|
+
))}
|
|
168
|
+
</select>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
{folderError && <Alert>{folderError}</Alert>}
|
|
172
|
+
</>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// With a single mailbox and no error this column would be empty: no 224 px of nothing beside the tasks' own menu.
|
|
176
|
+
const hasSidebar = mailboxes.length > 1 || !!folderError;
|
|
177
|
+
inner = (
|
|
178
|
+
<>
|
|
179
|
+
{hasSidebar && (
|
|
180
|
+
<aside className="hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3">
|
|
181
|
+
{sidebarContent("desktop")}
|
|
182
|
+
</aside>
|
|
183
|
+
)}
|
|
184
|
+
<Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Mailbox" fullScreen>
|
|
185
|
+
<div className="flex flex-col gap-3">{sidebarContent("mobile")}</div>
|
|
186
|
+
</Drawer>
|
|
187
|
+
<main className="flex-1 min-w-0 flex flex-col">
|
|
188
|
+
{hasSidebar && (
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
className="lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
|
|
192
|
+
aria-label="Open mailbox switcher"
|
|
193
|
+
onClick={() => setDrawerOpen(true)}
|
|
194
|
+
>
|
|
195
|
+
<HiOutlineBars3 size={20} aria-hidden="true" />
|
|
196
|
+
</button>
|
|
197
|
+
)}
|
|
198
|
+
<TasksShellContext.Provider value={contextValue}>{children}</TasksShellContext.Provider>
|
|
199
|
+
</main>
|
|
200
|
+
</>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<AppShell
|
|
206
|
+
active="tasks"
|
|
207
|
+
userUid={userUid}
|
|
208
|
+
authServerUrl={authServerUrl}
|
|
209
|
+
impersonating={impersonating}
|
|
210
|
+
impersonationBaseUrl={impersonationBaseUrl}
|
|
211
|
+
trusted={trusted}
|
|
212
|
+
trustedRoles={trustedRoles}
|
|
213
|
+
pluginNav={pluginNav}
|
|
214
|
+
>
|
|
215
|
+
{inner}
|
|
216
|
+
</AppShell>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useEffect, useRef, useState, type CSSProperties } from "react";
|
|
6
|
+
|
|
7
|
+
/** How long the new contents take to settle. Short, so a run of steps (a wheel kept turning) never waits on the last one. */
|
|
8
|
+
export const ENTER_SLIDE_MS = 140;
|
|
9
|
+
/** How far off, in pixels, and how faded the new contents start. */
|
|
10
|
+
const ENTER_OFFSET_PX = 56;
|
|
11
|
+
const ENTER_START_OPACITY = 0.35;
|
|
12
|
+
/** The pause that lets the browser draw the contents at their starting place before they move. */
|
|
13
|
+
const ENTER_SETTLE_MS = 20;
|
|
14
|
+
|
|
15
|
+
/** Whether the user asked their device for less motion. */
|
|
16
|
+
function prefersReducedMotion(): boolean {
|
|
17
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The look of a page that has just been swapped for the next or previous one by a vertical step (the wheel): the new contents start
|
|
22
|
+
* a little way off in the direction the step went - below for `1` (the next), above for `-1` - and faded, and settle into place.
|
|
23
|
+
* The old contents are not animated out: the swap is immediate, so a run of steps never queues, and a step in the middle of a
|
|
24
|
+
* settle just starts the next one from its start. Put `style` on the element that holds the contents and call `enter()` as the
|
|
25
|
+
* contents change. Nothing moves for someone who asked for less motion.
|
|
26
|
+
*/
|
|
27
|
+
export function useEnterSlide() {
|
|
28
|
+
const [offset, setOffset] = useState(0);
|
|
29
|
+
const [animated, setAnimated] = useState(false);
|
|
30
|
+
const timer = useRef<number | undefined>(undefined);
|
|
31
|
+
|
|
32
|
+
useEffect(() => () => window.clearTimeout(timer.current), []);
|
|
33
|
+
|
|
34
|
+
function enter(direction: 1 | -1) {
|
|
35
|
+
if (prefersReducedMotion()) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
window.clearTimeout(timer.current);
|
|
39
|
+
setAnimated(false);
|
|
40
|
+
setOffset(direction * ENTER_OFFSET_PX);
|
|
41
|
+
timer.current = window.setTimeout(() => {
|
|
42
|
+
setAnimated(true);
|
|
43
|
+
setOffset(0);
|
|
44
|
+
}, ENTER_SETTLE_MS);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const style: CSSProperties = {
|
|
48
|
+
transform: offset === 0 ? undefined : `translate3d(0, ${offset}px, 0)`,
|
|
49
|
+
opacity: offset === 0 ? undefined : ENTER_START_OPACITY,
|
|
50
|
+
transition: animated ? `transform ${ENTER_SLIDE_MS}ms ease-out, opacity ${ENTER_SLIDE_MS}ms ease-out` : "none",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return { style, enter };
|
|
54
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useRef, type TouchEvent } from "react";
|
|
6
|
+
|
|
7
|
+
/** Which way the finger moved: `"left"` is right to left. */
|
|
8
|
+
export type SwipeDirection = "left" | "right";
|
|
9
|
+
|
|
10
|
+
export interface UseSwipeOptions {
|
|
11
|
+
/** Nothing is tracked while this is `false` (a desktop pointer, select mode). Default `true`. */
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/** The directions that mean something; a drag the other way doesn't move at all. Default both. */
|
|
14
|
+
directions?: readonly SwipeDirection[];
|
|
15
|
+
/** How far the finger has to travel to commit, in px. Default `MIN_SWIPE_DISTANCE`, or a third of the element's width when that is more. */
|
|
16
|
+
distance?: number;
|
|
17
|
+
/** Called as the finger moves, with the horizontal offset (the direction filter applied), and with `0` once it lifts. */
|
|
18
|
+
onDrag?: (offset: number) => void;
|
|
19
|
+
/** Called once when a swipe commits. `onDrag(0)` follows. */
|
|
20
|
+
onSwipe: (direction: SwipeDirection) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The least distance that commits a swipe, and how far the finger must move before its direction is decided. */
|
|
24
|
+
export const MIN_SWIPE_DISTANCE = 80;
|
|
25
|
+
export const SWIPE_SLOP = 10;
|
|
26
|
+
/** A quicker flick than this (px per ms) commits after `FLING_MIN_DISTANCE` alone. */
|
|
27
|
+
export const FLING_VELOCITY = 0.5;
|
|
28
|
+
export const FLING_MIN_DISTANCE = 40;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A horizontal swipe by one finger, for the touch screens the app's phone layout is for: mouse and pen are left alone.
|
|
32
|
+
*
|
|
33
|
+
* Spread the handlers on the element and give it `touch-action: pan-y` (`style`), so the browser keeps scrolling vertically and leaves
|
|
34
|
+
* the horizontal movement to this. A gesture that starts out mostly vertical is a scroll and is never a swipe, however far it later
|
|
35
|
+
* strays sideways; one that starts out horizontal is a swipe, and stays one.
|
|
36
|
+
*/
|
|
37
|
+
export function useSwipe({ enabled = true, directions = ["left", "right"], distance, onDrag, onSwipe }: UseSwipeOptions) {
|
|
38
|
+
const start = useRef<{ x: number; y: number; time: number; width: number; mode?: "swipe" | "scroll" } | null>(null);
|
|
39
|
+
const offset = useRef(0);
|
|
40
|
+
const latest = useRef({ directions, distance, onDrag, onSwipe });
|
|
41
|
+
latest.current = { directions, distance, onDrag, onSwipe };
|
|
42
|
+
|
|
43
|
+
function clamp(dx: number): number {
|
|
44
|
+
const allowed = latest.current.directions;
|
|
45
|
+
return (dx < 0 && !allowed.includes("left")) || (dx > 0 && !allowed.includes("right")) ? 0 : dx;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function onTouchStart(event: TouchEvent<HTMLElement>) {
|
|
49
|
+
if (!enabled || event.touches.length !== 1) {
|
|
50
|
+
start.current = null;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const touch = event.touches[0];
|
|
54
|
+
start.current = { x: touch.clientX, y: touch.clientY, time: Date.now(), width: event.currentTarget.clientWidth };
|
|
55
|
+
offset.current = 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function onTouchMove(event: TouchEvent<HTMLElement>) {
|
|
59
|
+
const gesture = start.current;
|
|
60
|
+
if (!gesture || gesture.mode === "scroll") {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (event.touches.length !== 1) {
|
|
64
|
+
cancel();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const dx = event.touches[0].clientX - gesture.x;
|
|
68
|
+
const dy = event.touches[0].clientY - gesture.y;
|
|
69
|
+
if (!gesture.mode) {
|
|
70
|
+
if (Math.max(Math.abs(dx), Math.abs(dy)) < SWIPE_SLOP) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
gesture.mode = Math.abs(dx) > Math.abs(dy) * 1.5 ? "swipe" : "scroll";
|
|
74
|
+
if (gesture.mode === "scroll") {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
offset.current = clamp(dx);
|
|
79
|
+
latest.current.onDrag?.(offset.current);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function onTouchEnd() {
|
|
83
|
+
const gesture = start.current;
|
|
84
|
+
start.current = null;
|
|
85
|
+
if (!gesture || gesture.mode !== "swipe") {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const dx = offset.current;
|
|
89
|
+
offset.current = 0;
|
|
90
|
+
const travelled = Math.abs(dx);
|
|
91
|
+
const velocity = travelled / Math.max(1, Date.now() - gesture.time);
|
|
92
|
+
const needed = latest.current.distance ?? Math.max(MIN_SWIPE_DISTANCE, gesture.width / 3);
|
|
93
|
+
const committed = dx !== 0 && (travelled >= needed || (velocity >= FLING_VELOCITY && travelled >= FLING_MIN_DISTANCE));
|
|
94
|
+
if (committed) {
|
|
95
|
+
latest.current.onSwipe(dx < 0 ? "left" : "right");
|
|
96
|
+
}
|
|
97
|
+
latest.current.onDrag?.(0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function cancel() {
|
|
101
|
+
const moved = start.current?.mode === "swipe";
|
|
102
|
+
start.current = null;
|
|
103
|
+
offset.current = 0;
|
|
104
|
+
if (moved) {
|
|
105
|
+
latest.current.onDrag?.(0);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
handlers: enabled ? { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel: cancel } : {},
|
|
111
|
+
/** Leaves vertical panning to the browser. */
|
|
112
|
+
style: enabled ? ({ touchAction: "pan-y" } as const) : undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|