@remit/web-client 0.0.1
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/harness/build.mjs +84 -0
- package/harness/index.html +37 -0
- package/harness/vite-preset.ts +29 -0
- package/index.html +47 -0
- package/package.json +147 -0
- package/public/config.js +5 -0
- package/public/locales/de/common.json +1 -0
- package/public/locales/de/errors.json +1 -0
- package/public/locales/de/mail.json +13 -0
- package/public/locales/de/settings.json +1 -0
- package/public/locales/en/common.json +26 -0
- package/public/locales/en/errors.json +1 -0
- package/public/locales/en/mail.json +69 -0
- package/public/locales/en/settings.json +1 -0
- package/public/locales/fr/common.json +1 -0
- package/public/locales/fr/errors.json +1 -0
- package/public/locales/fr/mail.json +13 -0
- package/public/locales/fr/settings.json +1 -0
- package/public/locales/nl/common.json +1 -0
- package/public/locales/nl/errors.json +1 -0
- package/public/locales/nl/mail.json +13 -0
- package/public/locales/nl/settings.json +1 -0
- package/src/auth/AccountMenu.tsx +58 -0
- package/src/auth/AccountSession.tsx +19 -0
- package/src/auth/BetterAuthShell.tsx +198 -0
- package/src/auth/account-menu-mode.test.ts +64 -0
- package/src/auth/account-menu-mode.ts +28 -0
- package/src/auth/amplify-config.test.ts +158 -0
- package/src/auth/amplify-config.ts +60 -0
- package/src/auth/auth-error.test.ts +109 -0
- package/src/auth/auth-error.ts +107 -0
- package/src/auth/auth-interceptor.test.ts +86 -0
- package/src/auth/auth-interceptor.ts +23 -0
- package/src/auth/auth.css +334 -0
- package/src/auth/better-auth/BetterAuthAccount.tsx +25 -0
- package/src/auth/better-auth-config.ts +85 -0
- package/src/auth/better-auth-provider.tsx +20 -0
- package/src/auth/cognito/CognitoAccount.tsx +48 -0
- package/src/auth/cognito/CognitoShell.tsx +266 -0
- package/src/auth/cognito/cognito-token.test.ts +110 -0
- package/src/auth/cognito/cognito-token.ts +47 -0
- package/src/auth/cognito/cognito.css +21 -0
- package/src/auth/cognito-provider.tsx +22 -0
- package/src/auth/combined-provider.tsx +42 -0
- package/src/auth/composition.test.ts +136 -0
- package/src/auth/provider.tsx +63 -0
- package/src/auth/sign-out-visibility.test.ts +40 -0
- package/src/auth/sign-out-visibility.ts +10 -0
- package/src/auth/sign-up-disabled.test.ts +23 -0
- package/src/components/compose/AddressField.tsx +237 -0
- package/src/components/compose/ComposeBody.tsx +36 -0
- package/src/components/compose/ComposeForm.tsx +676 -0
- package/src/components/compose/ComposeProvider.tsx +133 -0
- package/src/components/compose/ComposeSmtpMissingBanner.tsx +49 -0
- package/src/components/compose/FromSelector.tsx +67 -0
- package/src/components/compose/FullCompose.tsx +82 -0
- package/src/components/compose/InlineCompose.tsx +29 -0
- package/src/components/compose/MobileComposeSheet.tsx +152 -0
- package/src/components/compose/PlateEditor.tsx +86 -0
- package/src/components/compose/PlateToolbar.tsx +110 -0
- package/src/components/compose/SubjectField.tsx +19 -0
- package/src/components/compose/draft-discard-banner.test.ts +76 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/sanitize-quote-html.ts +23 -0
- package/src/components/layout/AppShellSkeleton.tsx +53 -0
- package/src/components/layout/ComposeFab.tsx +86 -0
- package/src/components/layout/Drawer.tsx +105 -0
- package/src/components/mail/AutoMovedIndicator.tsx +48 -0
- package/src/components/mail/BlockedImagesNotice.render.test.ts +65 -0
- package/src/components/mail/BlockedImagesNotice.tsx +41 -0
- package/src/components/mail/BriefPane.tsx +289 -0
- package/src/components/mail/ConversationView.tsx +462 -0
- package/src/components/mail/DailyBrief.tsx +451 -0
- package/src/components/mail/DraftsView.tsx +279 -0
- package/src/components/mail/FlaggedList.tsx +170 -0
- package/src/components/mail/FlaggedPane.tsx +217 -0
- package/src/components/mail/IntelligencePane.test.ts +123 -0
- package/src/components/mail/IntelligencePane.tsx +441 -0
- package/src/components/mail/MailListHeader.tsx +187 -0
- package/src/components/mail/MailNav.tsx +70 -0
- package/src/components/mail/MailSidebarAdapter.tsx +351 -0
- package/src/components/mail/MailViewChrome.tsx +95 -0
- package/src/components/mail/MailboxPane.tsx +1219 -0
- package/src/components/mail/MessageActionMenu.tsx +368 -0
- package/src/components/mail/MessageBody.tsx +207 -0
- package/src/components/mail/MessageBodyErrorBanner.render.test.ts +85 -0
- package/src/components/mail/MessageBodyErrorBanner.test.ts +86 -0
- package/src/components/mail/MessageBodyErrorBanner.tsx +90 -0
- package/src/components/mail/MessageCard.tsx +401 -0
- package/src/components/mail/MessageDetail.tsx +153 -0
- package/src/components/mail/MessageList.tsx +804 -0
- package/src/components/mail/MessageListItem.tsx +267 -0
- package/src/components/mail/MessageToolbar.render.test.ts +57 -0
- package/src/components/mail/MessageToolbar.tsx +166 -0
- package/src/components/mail/MobileMessageBar.tsx +142 -0
- package/src/components/mail/MoveToTrigger.tsx +250 -0
- package/src/components/mail/NavMenuButton.tsx +25 -0
- package/src/components/mail/OutboxPane.tsx +531 -0
- package/src/components/mail/PullToRefresh.tsx +24 -0
- package/src/components/mail/RawMessageView.tsx +84 -0
- package/src/components/mail/ReadingPaneEmpty.render.test.ts +42 -0
- package/src/components/mail/SelectionToolbar.tsx +139 -0
- package/src/components/mail/SpamRescue.render.test.ts +35 -0
- package/src/components/mail/SpamRescue.tsx +138 -0
- package/src/components/mail/SwipeableMessageRow.tsx +148 -0
- package/src/components/mail/message-body-error-banner-content.ts +87 -0
- package/src/components/mail/organize/OrganizeDialog.render.test.ts +37 -0
- package/src/components/mail/organize/OrganizeDialog.tsx +83 -0
- package/src/components/mail/organize/OrganizePanel.render.test.ts +102 -0
- package/src/components/mail/organize/OrganizePanel.tsx +344 -0
- package/src/components/onboarding/OnboardingWizard.tsx +1547 -0
- package/src/components/settings/AccountFormPanel.test.ts +72 -0
- package/src/components/settings/AccountFormPanel.tsx +942 -0
- package/src/components/settings/DangerZone.tsx +30 -0
- package/src/components/settings/DeleteAccountDialog.render.test.ts +47 -0
- package/src/components/settings/DeleteAccountDialog.tsx +224 -0
- package/src/components/settings/FiltersList.render.test.ts +71 -0
- package/src/components/settings/FiltersList.tsx +93 -0
- package/src/components/settings/account-form-helpers.test.ts +172 -0
- package/src/components/settings/account-form-helpers.ts +105 -0
- package/src/components/ui/AppVersion.tsx +50 -0
- package/src/components/ui/BugReportButton.tsx +45 -0
- package/src/components/ui/ConfirmDialog.tsx +125 -0
- package/src/components/ui/DropdownMenu.tsx +105 -0
- package/src/components/ui/EmptyState.tsx +11 -0
- package/src/components/ui/ErrorBanner.tsx +88 -0
- package/src/components/ui/ErrorBannerProvider.tsx +77 -0
- package/src/components/ui/ErrorBannerStack.tsx +34 -0
- package/src/components/ui/ErrorState.tsx +80 -0
- package/src/components/ui/FatalErrorOverlay.render.test.ts +66 -0
- package/src/components/ui/FatalErrorOverlay.tsx +151 -0
- package/src/components/ui/KeyboardShortcutsModal.tsx +120 -0
- package/src/components/ui/SlidePanel.tsx +72 -0
- package/src/components/ui/error-banners.test.ts +207 -0
- package/src/components/ui/error-banners.ts +111 -0
- package/src/hooks/queries/keys.ts +38 -0
- package/src/hooks/useArchiveMailbox.ts +104 -0
- package/src/hooks/useAutoMovedBadge.ts +77 -0
- package/src/hooks/useCurrentMailboxName.ts +99 -0
- package/src/hooks/useDebouncedValue.ts +12 -0
- package/src/hooks/useDeleteMessages.test.ts +103 -0
- package/src/hooks/useDeleteMessages.ts +226 -0
- package/src/hooks/useFilters.ts +112 -0
- package/src/hooks/useIntelligenceData.test.ts +269 -0
- package/src/hooks/useIntelligenceData.ts +284 -0
- package/src/hooks/useIsDark.ts +27 -0
- package/src/hooks/useKeyboardNavigation.ts +192 -0
- package/src/hooks/useLayoutTier.test.ts +47 -0
- package/src/hooks/useLayoutTier.ts +44 -0
- package/src/hooks/useLongPress.test.ts +291 -0
- package/src/hooks/useLongPress.ts +87 -0
- package/src/hooks/useMailboxAccount.ts +62 -0
- package/src/hooks/useMailboxNameIndex.ts +32 -0
- package/src/hooks/useMarkAsRead.test.ts +165 -0
- package/src/hooks/useMarkAsRead.ts +319 -0
- package/src/hooks/useMediaQuery.ts +39 -0
- package/src/hooks/useMessageBodyContent.test.ts +370 -0
- package/src/hooks/useMessageBodyContent.ts +380 -0
- package/src/hooks/useMoveMessages.test.ts +63 -0
- package/src/hooks/useMoveMessages.ts +221 -0
- package/src/hooks/useOrganizeJob.ts +90 -0
- package/src/hooks/useOrganizePreview.ts +40 -0
- package/src/hooks/useRescueCandidates.test.ts +217 -0
- package/src/hooks/useRescueCandidates.ts +48 -0
- package/src/hooks/useSaveDraft.ts +118 -0
- package/src/hooks/useSelection.test.ts +75 -0
- package/src/hooks/useSelection.ts +211 -0
- package/src/hooks/useSemanticSearch.ts +94 -0
- package/src/hooks/useSignature.ts +69 -0
- package/src/hooks/useStaleAccountSync.test.ts +211 -0
- package/src/hooks/useStaleAccountSync.ts +246 -0
- package/src/hooks/useSwipeNavigation.test.ts +35 -0
- package/src/hooks/useSwipeNavigation.ts +99 -0
- package/src/hooks/useToggleStar.ts +187 -0
- package/src/hooks/useToggleTrusted.test.ts +102 -0
- package/src/hooks/useToggleTrusted.ts +161 -0
- package/src/hooks/useTriageKeyboard.ts +103 -0
- package/src/hooks/useTriggerSync.test.ts +24 -0
- package/src/hooks/useTriggerSync.ts +58 -0
- package/src/hooks/useUpdateAddressFlags.ts +125 -0
- package/src/hooks/useVisualViewport.test.ts +127 -0
- package/src/hooks/useVisualViewport.ts +51 -0
- package/src/index.css +19 -0
- package/src/lib/account-order.test.ts +84 -0
- package/src/lib/account-order.ts +13 -0
- package/src/lib/adjacent-message.test.ts +43 -0
- package/src/lib/adjacent-message.ts +20 -0
- package/src/lib/api.ts +82 -0
- package/src/lib/app-info.ts +15 -0
- package/src/lib/auto-moved.test.ts +140 -0
- package/src/lib/auto-moved.ts +68 -0
- package/src/lib/autodiscovery.test.ts +94 -0
- package/src/lib/autodiscovery.ts +354 -0
- package/src/lib/brief.test.ts +366 -0
- package/src/lib/brief.ts +180 -0
- package/src/lib/bug-report.test.ts +213 -0
- package/src/lib/bug-report.ts +176 -0
- package/src/lib/client.ts +36 -0
- package/src/lib/console-errors.test.ts +64 -0
- package/src/lib/console-errors.ts +92 -0
- package/src/lib/conversation-target.test.ts +68 -0
- package/src/lib/conversation-target.ts +59 -0
- package/src/lib/display-category.ts +20 -0
- package/src/lib/drafts.test.ts +241 -0
- package/src/lib/drafts.ts +121 -0
- package/src/lib/error-classifier.test.ts +133 -0
- package/src/lib/error-classifier.ts +96 -0
- package/src/lib/fatal-error.test.ts +97 -0
- package/src/lib/fatal-error.ts +165 -0
- package/src/lib/folder-roles.test.ts +113 -0
- package/src/lib/folder-roles.ts +112 -0
- package/src/lib/format.test.ts +115 -0
- package/src/lib/format.ts +198 -0
- package/src/lib/i18n.ts +36 -0
- package/src/lib/intelligence-pref.test.ts +57 -0
- package/src/lib/intelligence-pref.ts +25 -0
- package/src/lib/keymap-dispatch.test.ts +178 -0
- package/src/lib/keymap-dispatch.ts +187 -0
- package/src/lib/keymap.test.ts +49 -0
- package/src/lib/keymap.ts +178 -0
- package/src/lib/mail-context.ts +64 -0
- package/src/lib/mail-route.test.ts +80 -0
- package/src/lib/mail-route.ts +44 -0
- package/src/lib/message-body-source.test.ts +98 -0
- package/src/lib/message-body-source.ts +48 -0
- package/src/lib/move-targets.test.ts +195 -0
- package/src/lib/move-targets.ts +74 -0
- package/src/lib/onboarding-completion.test.ts +64 -0
- package/src/lib/onboarding-completion.ts +23 -0
- package/src/lib/organize/filter-status.test.ts +99 -0
- package/src/lib/organize/filter-status.ts +86 -0
- package/src/lib/organize/organize-copy.test.ts +93 -0
- package/src/lib/organize/organize-copy.ts +61 -0
- package/src/lib/organize/organize-model.test.ts +104 -0
- package/src/lib/organize/organize-model.ts +105 -0
- package/src/lib/organize/organize-poll.test.ts +37 -0
- package/src/lib/organize/organize-poll.ts +28 -0
- package/src/lib/outbox-status.test.ts +64 -0
- package/src/lib/outbox-status.ts +75 -0
- package/src/lib/plate-serializer.ts +164 -0
- package/src/lib/provider-presets.test.ts +96 -0
- package/src/lib/provider-presets.ts +81 -0
- package/src/lib/query-error-handler.test.ts +144 -0
- package/src/lib/query-error-handler.ts +34 -0
- package/src/lib/query-escalation.integration.test.ts +138 -0
- package/src/lib/recent-searches.test.ts +53 -0
- package/src/lib/recent-searches.ts +34 -0
- package/src/lib/rescue-candidates.test.ts +94 -0
- package/src/lib/rescue-candidates.ts +35 -0
- package/src/lib/rescue-telemetry.test.ts +75 -0
- package/src/lib/rescue-telemetry.ts +55 -0
- package/src/lib/rum-adapter.test.ts +299 -0
- package/src/lib/rum-adapter.ts +107 -0
- package/src/lib/saved-searches.test.ts +80 -0
- package/src/lib/saved-searches.ts +52 -0
- package/src/lib/search-pending.test.ts +61 -0
- package/src/lib/search-pending.ts +24 -0
- package/src/lib/search-query.test.ts +35 -0
- package/src/lib/search-query.ts +11 -0
- package/src/lib/search-result.test.ts +160 -0
- package/src/lib/search-result.ts +102 -0
- package/src/lib/search-schemas.ts +34 -0
- package/src/lib/search-token-index.test.ts +112 -0
- package/src/lib/search-token-index.ts +56 -0
- package/src/lib/search-tokens.test.ts +218 -0
- package/src/lib/search-tokens.ts +133 -0
- package/src/lib/telemetry-context.tsx +9 -0
- package/src/lib/telemetry.ts +17 -0
- package/src/lib/theme-preference.ts +46 -0
- package/src/lib/theme.ts +33 -0
- package/src/lib/utils.ts +4 -0
- package/src/main.tsx +10 -0
- package/src/routeTree.gen.ts +392 -0
- package/src/router.test.ts +81 -0
- package/src/router.tsx +31 -0
- package/src/routes/__root.tsx +69 -0
- package/src/routes/index.tsx +7 -0
- package/src/routes/mail/$mailboxId.tsx +27 -0
- package/src/routes/mail/flagged.tsx +37 -0
- package/src/routes/mail/index.tsx +41 -0
- package/src/routes/mail/outbox.tsx +13 -0
- package/src/routes/mail.tsx +413 -0
- package/src/routes/onboarding.tsx +35 -0
- package/src/routes/settings/-accounts.test.ts +65 -0
- package/src/routes/settings/accounts.tsx +542 -0
- package/src/routes/settings/advanced.tsx +55 -0
- package/src/routes/settings/appearance.tsx +140 -0
- package/src/routes/settings/filters.tsx +141 -0
- package/src/routes/settings/folders.tsx +216 -0
- package/src/routes/settings/index.tsx +5 -0
- package/src/routes/settings/senders.tsx +466 -0
- package/src/routes/settings/suggested-vips.tsx +10 -0
- package/src/routes/settings.tsx +66 -0
- package/src/runtime-config.ts +75 -0
- package/src/shell/index.tsx +98 -0
- package/src/types/index.ts +73 -0
- package/src/vite-env.d.ts +4 -0
- package/vite.base.ts +43 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configOperationsGetConfigOptions,
|
|
3
|
+
messageOperationsDescribeMessageOptions,
|
|
4
|
+
threadDetailOperationsListThreadMessagesOptions,
|
|
5
|
+
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
6
|
+
import type { RemitImapMessageAuthenticity } from "@remit/api-http-client/types.gen.ts";
|
|
7
|
+
import { MobileReadingPane } from "@remit/ui";
|
|
8
|
+
import { useQuery } from "@tanstack/react-query";
|
|
9
|
+
import { ShieldAlert } from "lucide-react";
|
|
10
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
11
|
+
import type { ComposeMode } from "@/components/compose/ComposeProvider";
|
|
12
|
+
import { InlineCompose } from "@/components/compose/InlineCompose";
|
|
13
|
+
import { EmptyState } from "@/components/ui/EmptyState";
|
|
14
|
+
import { ErrorState } from "@/components/ui/ErrorState";
|
|
15
|
+
import { useKeyboardNavigation } from "@/hooks/useKeyboardNavigation";
|
|
16
|
+
import { useMailboxAccount } from "@/hooks/useMailboxAccount";
|
|
17
|
+
import { useMarkAsRead } from "@/hooks/useMarkAsRead";
|
|
18
|
+
import { useIsDesktop } from "@/hooks/useMediaQuery";
|
|
19
|
+
import { useSwipeNavigation } from "@/hooks/useSwipeNavigation";
|
|
20
|
+
import { useToggleStar } from "@/hooks/useToggleStar";
|
|
21
|
+
import { MessageCard } from "./MessageCard";
|
|
22
|
+
|
|
23
|
+
interface ConversationViewProps {
|
|
24
|
+
threadId: string;
|
|
25
|
+
mailboxId: string;
|
|
26
|
+
subject?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The messageId of the row the user clicked in the thread list. When
|
|
29
|
+
* provided, this message is expanded on open (in addition to the newest
|
|
30
|
+
* message) so that viewing an older unread message marks it as read.
|
|
31
|
+
*/
|
|
32
|
+
selectedMessageId?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Authenticity signal from the thread row (DKIM mismatch). When present
|
|
35
|
+
* and `dkimMismatch` is true, a danger banner renders above the message
|
|
36
|
+
* body with a "Why?" link that opens the intelligence sidebar.
|
|
37
|
+
*/
|
|
38
|
+
authenticity?: RemitImapMessageAuthenticity;
|
|
39
|
+
/**
|
|
40
|
+
* Callback for the "Why?" link in the authenticity banner — opens /
|
|
41
|
+
* focuses the intelligence sidebar.
|
|
42
|
+
*/
|
|
43
|
+
onOpenIntelligence?: () => void;
|
|
44
|
+
/**
|
|
45
|
+
* Mobile callers pass `onBack` to render a sticky Back button at the
|
|
46
|
+
* bottom of the conversation. Desktop callers omit it — the message
|
|
47
|
+
* list is always visible in the resizable side pane.
|
|
48
|
+
*/
|
|
49
|
+
onBack?: () => void;
|
|
50
|
+
/**
|
|
51
|
+
* When set, immediately opens the inline compose in this mode. The
|
|
52
|
+
* parent (e.g. the reading-pane toolbar) sets this to wire the
|
|
53
|
+
* top-bar reply/reply-all/forward buttons into the inline compose
|
|
54
|
+
* that lives inside the conversation. Reset by calling
|
|
55
|
+
* `onComposeClose` after the compose opens.
|
|
56
|
+
*/
|
|
57
|
+
composeRequest?: ComposeMode | null;
|
|
58
|
+
/**
|
|
59
|
+
* Called once the conversation has acted on `composeRequest` (or
|
|
60
|
+
* whenever the inline compose is dismissed). The caller should reset
|
|
61
|
+
* `composeRequest` to `null`.
|
|
62
|
+
*/
|
|
63
|
+
onComposeClose?: () => void;
|
|
64
|
+
/**
|
|
65
|
+
* Mobile only: swipe-left handler to open the next message in the list the
|
|
66
|
+
* thread was opened from. Omitted at the end of the list so the gesture
|
|
67
|
+
* no-ops gracefully.
|
|
68
|
+
*/
|
|
69
|
+
onSwipeNext?: () => void;
|
|
70
|
+
/**
|
|
71
|
+
* Mobile only: swipe-right handler to open the previous message in the list.
|
|
72
|
+
* Omitted at the start of the list.
|
|
73
|
+
*/
|
|
74
|
+
onSwipePrevious?: () => void;
|
|
75
|
+
/** Whether the intelligence drawer is currently open (drives the ⓘ button pressed state). */
|
|
76
|
+
mobileIntelligenceOpen?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const LoadingSkeleton = () => (
|
|
80
|
+
<div className="animate-pulse p-4">
|
|
81
|
+
<div className="h-6 bg-surface-sunken rounded w-3/4 mb-5" />
|
|
82
|
+
<div className="space-y-4">
|
|
83
|
+
{Array.from({ length: 2 }).map((_, i) => (
|
|
84
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: list is static, no stable id
|
|
85
|
+
<div key={i} className="flex gap-3 py-3">
|
|
86
|
+
<div className="size-10 bg-surface-sunken rounded-full shrink-0" />
|
|
87
|
+
<div className="flex-1">
|
|
88
|
+
<div className="h-4 bg-surface-sunken rounded w-32 mb-2" />
|
|
89
|
+
<div className="h-3 bg-surface-sunken rounded w-48" />
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Danger banner rendered above the thread body when DKIM mismatch is detected.
|
|
99
|
+
* Design spec (03-reading-and-intelligence.md): "a danger banner renders above
|
|
100
|
+
* the body: 'This message claims to be a company but was sent from a personal
|
|
101
|
+
* mailbox.' with a 'Why?' link that opens/highlights this section."
|
|
102
|
+
*/
|
|
103
|
+
function AuthenticityBanner({
|
|
104
|
+
authenticity,
|
|
105
|
+
onOpenIntelligence,
|
|
106
|
+
}: {
|
|
107
|
+
authenticity: RemitImapMessageAuthenticity;
|
|
108
|
+
onOpenIntelligence?: () => void;
|
|
109
|
+
}) {
|
|
110
|
+
if (!authenticity.dkimMismatch) return null;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="flex items-start gap-2 rounded-none border-b border-danger/20 bg-danger-soft px-5 py-2.5 text-sm">
|
|
114
|
+
<ShieldAlert className="mt-0.5 size-4 shrink-0 text-danger" />
|
|
115
|
+
<p className="flex-1 leading-snug text-fg">
|
|
116
|
+
This message claims to be from{" "}
|
|
117
|
+
<span className="font-semibold">{authenticity.fromDomain}</span> but was
|
|
118
|
+
sent
|
|
119
|
+
{authenticity.dkimDomain
|
|
120
|
+
? ` via ${authenticity.dkimDomain}`
|
|
121
|
+
: " from a different domain"}
|
|
122
|
+
.
|
|
123
|
+
</p>
|
|
124
|
+
{onOpenIntelligence && (
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
onClick={onOpenIntelligence}
|
|
128
|
+
className="shrink-0 text-2xs font-medium text-danger hover:underline"
|
|
129
|
+
>
|
|
130
|
+
Why?
|
|
131
|
+
</button>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const ConversationView = ({
|
|
138
|
+
threadId,
|
|
139
|
+
mailboxId,
|
|
140
|
+
subject,
|
|
141
|
+
selectedMessageId,
|
|
142
|
+
authenticity,
|
|
143
|
+
onOpenIntelligence,
|
|
144
|
+
onBack,
|
|
145
|
+
composeRequest,
|
|
146
|
+
onComposeClose,
|
|
147
|
+
onSwipeNext,
|
|
148
|
+
onSwipePrevious,
|
|
149
|
+
mobileIntelligenceOpen,
|
|
150
|
+
}: ConversationViewProps) => {
|
|
151
|
+
const isDesktop = useIsDesktop();
|
|
152
|
+
const { handlers: swipeHandlers } = useSwipeNavigation({
|
|
153
|
+
onSwipeLeft: onSwipeNext,
|
|
154
|
+
onSwipeRight: onSwipePrevious,
|
|
155
|
+
});
|
|
156
|
+
const { accountId: mailboxAccountId } = useMailboxAccount(mailboxId);
|
|
157
|
+
const {
|
|
158
|
+
data: messagesResponse,
|
|
159
|
+
isLoading,
|
|
160
|
+
isError,
|
|
161
|
+
error,
|
|
162
|
+
refetch,
|
|
163
|
+
} = useQuery({
|
|
164
|
+
...threadDetailOperationsListThreadMessagesOptions({
|
|
165
|
+
path: { threadId },
|
|
166
|
+
query: { order: "desc", mailboxId },
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const messages = useMemo(
|
|
171
|
+
() => messagesResponse?.items ?? [],
|
|
172
|
+
[messagesResponse?.items],
|
|
173
|
+
);
|
|
174
|
+
const messageRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
|
175
|
+
|
|
176
|
+
// Track which messages are expanded
|
|
177
|
+
// By default, the first (newest) message is expanded
|
|
178
|
+
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
|
|
179
|
+
const [currentThreadId, setCurrentThreadId] = useState<string | null>(null);
|
|
180
|
+
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
181
|
+
|
|
182
|
+
// Reset and expand first message when thread changes or messages load.
|
|
183
|
+
// Also expand the selected message (the row the user clicked in the list)
|
|
184
|
+
// so that an older unread message is visible and gets marked as read.
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (messages.length > 0 && threadId !== currentThreadId) {
|
|
187
|
+
setCurrentThreadId(threadId);
|
|
188
|
+
const initialExpanded = new Set([messages[0].threadMessageId]);
|
|
189
|
+
if (selectedMessageId) {
|
|
190
|
+
const selected = messages.find(
|
|
191
|
+
(m) => m.messageId === selectedMessageId,
|
|
192
|
+
);
|
|
193
|
+
if (selected) initialExpanded.add(selected.threadMessageId);
|
|
194
|
+
}
|
|
195
|
+
setExpandedIds(initialExpanded);
|
|
196
|
+
setFocusedIndex(0);
|
|
197
|
+
}
|
|
198
|
+
}, [threadId, messages, currentThreadId, selectedMessageId]);
|
|
199
|
+
|
|
200
|
+
const toggleExpanded = useCallback((threadMessageId: string) => {
|
|
201
|
+
setExpandedIds((prev) => {
|
|
202
|
+
const next = new Set(prev);
|
|
203
|
+
if (next.has(threadMessageId)) {
|
|
204
|
+
next.delete(threadMessageId);
|
|
205
|
+
} else {
|
|
206
|
+
next.add(threadMessageId);
|
|
207
|
+
}
|
|
208
|
+
return next;
|
|
209
|
+
});
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
// Scroll focused message into view
|
|
213
|
+
const scrollToMessage = useCallback(
|
|
214
|
+
(index: number) => {
|
|
215
|
+
const message = messages[index];
|
|
216
|
+
if (!message) return;
|
|
217
|
+
const element = messageRefs.current.get(message.threadMessageId);
|
|
218
|
+
element?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
219
|
+
},
|
|
220
|
+
[messages],
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Keyboard navigation handlers
|
|
224
|
+
const focusNext = useCallback(() => {
|
|
225
|
+
if (messages.length === 0) return;
|
|
226
|
+
const nextIndex = Math.min(focusedIndex + 1, messages.length - 1);
|
|
227
|
+
setFocusedIndex(nextIndex);
|
|
228
|
+
scrollToMessage(nextIndex);
|
|
229
|
+
}, [messages.length, focusedIndex, scrollToMessage]);
|
|
230
|
+
|
|
231
|
+
const focusPrevious = useCallback(() => {
|
|
232
|
+
if (messages.length === 0) return;
|
|
233
|
+
const prevIndex = Math.max(focusedIndex - 1, 0);
|
|
234
|
+
setFocusedIndex(prevIndex);
|
|
235
|
+
scrollToMessage(prevIndex);
|
|
236
|
+
}, [messages.length, focusedIndex, scrollToMessage]);
|
|
237
|
+
|
|
238
|
+
const toggleFocusedMessage = useCallback(() => {
|
|
239
|
+
const message = messages[focusedIndex];
|
|
240
|
+
if (message) {
|
|
241
|
+
toggleExpanded(message.threadMessageId);
|
|
242
|
+
}
|
|
243
|
+
}, [messages, focusedIndex, toggleExpanded]);
|
|
244
|
+
|
|
245
|
+
const { data: config } = useQuery({
|
|
246
|
+
...configOperationsGetConfigOptions(),
|
|
247
|
+
staleTime: Infinity,
|
|
248
|
+
});
|
|
249
|
+
const activeAccount = config?.accounts?.[0];
|
|
250
|
+
const smtpConfigured = !!activeAccount?.smtpHost;
|
|
251
|
+
|
|
252
|
+
// Mark messages as read immediately when expanded.
|
|
253
|
+
useMarkAsRead({
|
|
254
|
+
messages,
|
|
255
|
+
expandedIds,
|
|
256
|
+
threadId,
|
|
257
|
+
mailboxId,
|
|
258
|
+
accountId: activeAccount?.accountId,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Star toggle functionality
|
|
262
|
+
const {
|
|
263
|
+
toggleStar,
|
|
264
|
+
isPending: isStarPending,
|
|
265
|
+
pendingMessageId,
|
|
266
|
+
} = useToggleStar({
|
|
267
|
+
threadId,
|
|
268
|
+
mailboxId,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Compose state for inline reply/forward.
|
|
272
|
+
// Controlled via the `composeRequest` prop (toolbar wire-up) or
|
|
273
|
+
// locally via r/a/f keyboard shortcuts.
|
|
274
|
+
const [composeMode, setComposeMode] = useState<ComposeMode | null>(null);
|
|
275
|
+
|
|
276
|
+
// When the toolbar passes a composeRequest, open the inline compose.
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
if (composeRequest && composeRequest !== "new") {
|
|
279
|
+
setComposeMode(composeRequest);
|
|
280
|
+
// Notify the parent that the request has been consumed.
|
|
281
|
+
onComposeClose?.();
|
|
282
|
+
}
|
|
283
|
+
}, [composeRequest, onComposeClose]);
|
|
284
|
+
|
|
285
|
+
const lastMessage = messages[0];
|
|
286
|
+
const { data: lastMessageData } = useQuery({
|
|
287
|
+
...messageOperationsDescribeMessageOptions({
|
|
288
|
+
path: { messageId: lastMessage?.messageId ?? "" },
|
|
289
|
+
}),
|
|
290
|
+
enabled: !!lastMessage && composeMode !== null,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const handleReply = useCallback(() => {
|
|
294
|
+
setComposeMode("reply");
|
|
295
|
+
}, []);
|
|
296
|
+
|
|
297
|
+
const handleReplyAll = useCallback(() => {
|
|
298
|
+
setComposeMode("reply_all");
|
|
299
|
+
}, []);
|
|
300
|
+
|
|
301
|
+
const handleForward = useCallback(() => {
|
|
302
|
+
setComposeMode("forward");
|
|
303
|
+
}, []);
|
|
304
|
+
|
|
305
|
+
const handleCloseCompose = useCallback(() => {
|
|
306
|
+
setComposeMode(null);
|
|
307
|
+
onComposeClose?.();
|
|
308
|
+
}, [onComposeClose]);
|
|
309
|
+
|
|
310
|
+
// Register keyboard shortcuts
|
|
311
|
+
useKeyboardNavigation({
|
|
312
|
+
enabled: !isLoading && messages.length > 0 && composeMode === null,
|
|
313
|
+
bindings: [
|
|
314
|
+
{ key: "j", handler: focusNext, preventDefault: true },
|
|
315
|
+
{ key: "ArrowDown", handler: focusNext, preventDefault: true },
|
|
316
|
+
{ key: "k", handler: focusPrevious, preventDefault: true },
|
|
317
|
+
{ key: "ArrowUp", handler: focusPrevious, preventDefault: true },
|
|
318
|
+
{ key: "Enter", handler: toggleFocusedMessage, preventDefault: true },
|
|
319
|
+
{ key: "o", handler: toggleFocusedMessage, preventDefault: true },
|
|
320
|
+
...(smtpConfigured
|
|
321
|
+
? [
|
|
322
|
+
{ key: "r", handler: handleReply, preventDefault: true },
|
|
323
|
+
{
|
|
324
|
+
key: "R",
|
|
325
|
+
handler: handleReplyAll,
|
|
326
|
+
noModifiers: false,
|
|
327
|
+
preventDefault: true,
|
|
328
|
+
},
|
|
329
|
+
{ key: "f", handler: handleForward, preventDefault: true },
|
|
330
|
+
]
|
|
331
|
+
: []),
|
|
332
|
+
],
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (isLoading) {
|
|
336
|
+
return <LoadingSkeleton />;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (isError) {
|
|
340
|
+
return (
|
|
341
|
+
<div className="flex h-full items-center justify-center">
|
|
342
|
+
<ErrorState
|
|
343
|
+
title="Couldn't load this conversation"
|
|
344
|
+
error={error}
|
|
345
|
+
onRetry={() => refetch()}
|
|
346
|
+
/>
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (messages.length === 0) {
|
|
352
|
+
return (
|
|
353
|
+
<div className="flex h-full items-center justify-center">
|
|
354
|
+
<EmptyState message="No messages in this thread" />
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const displaySubject = subject || messages[0]?.subject || "(No subject)";
|
|
360
|
+
const messageCount = messages.length;
|
|
361
|
+
|
|
362
|
+
// Message list wrapper — no extra x-padding; each MessageCard handles
|
|
363
|
+
// its own px-5 inset (matches the AppShell ReadingPane geometry). On mobile
|
|
364
|
+
// each expanded card owns a per-message action bar; reply verbs are wired
|
|
365
|
+
// only when SMTP is configured (otherwise the buttons no-op).
|
|
366
|
+
const renderMessages = (mobile: boolean) => (
|
|
367
|
+
<div>
|
|
368
|
+
{messages.map((message, index) => (
|
|
369
|
+
<div
|
|
370
|
+
key={message.threadMessageId}
|
|
371
|
+
ref={(el) => {
|
|
372
|
+
if (el) messageRefs.current.set(message.threadMessageId, el);
|
|
373
|
+
}}
|
|
374
|
+
>
|
|
375
|
+
<MessageCard
|
|
376
|
+
threadMessage={message}
|
|
377
|
+
isExpanded={expandedIds.has(message.threadMessageId)}
|
|
378
|
+
isFocused={index === focusedIndex}
|
|
379
|
+
onToggle={() => toggleExpanded(message.threadMessageId)}
|
|
380
|
+
onToggleStar={() => toggleStar(message.messageId, message.hasStars)}
|
|
381
|
+
isStarPending={
|
|
382
|
+
isStarPending && pendingMessageId === message.messageId
|
|
383
|
+
}
|
|
384
|
+
accountId={mailboxAccountId}
|
|
385
|
+
mobile={mobile}
|
|
386
|
+
onReply={mobile && smtpConfigured ? handleReply : undefined}
|
|
387
|
+
onReplyAll={mobile && smtpConfigured ? handleReplyAll : undefined}
|
|
388
|
+
onForward={mobile && smtpConfigured ? handleForward : undefined}
|
|
389
|
+
/>
|
|
390
|
+
</div>
|
|
391
|
+
))}
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// Subject block: matches the AppShell ReadingPane reference exactly —
|
|
396
|
+
// px-5 pt-5 pb-3, text-lg leading-snug, 2xs count. Subject scrolls
|
|
397
|
+
// with the thread body; no subject in the toolbar chrome.
|
|
398
|
+
const header = (
|
|
399
|
+
<header className="border-b border-line px-5 pt-5 pb-3">
|
|
400
|
+
<h1 className="max-w-2xl text-lg font-semibold leading-snug text-fg">
|
|
401
|
+
{displaySubject}
|
|
402
|
+
</h1>
|
|
403
|
+
<p className="mt-1 text-2xs text-fg-subtle">
|
|
404
|
+
{messageCount} {messageCount === 1 ? "message" : "messages"}
|
|
405
|
+
</p>
|
|
406
|
+
</header>
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// Mobile: the kit MobileReadingPane owns the chrome — a top app bar with
|
|
410
|
+
// back, the email subject and the intelligence toggle (the host owns the
|
|
411
|
+
// drawer). Each expanded card owns its per-message action bar; there is no
|
|
412
|
+
// thread-level reply footer. The phishing warning leads the scroll content;
|
|
413
|
+
// horizontal swipe between messages is wired through touchHandlers (#693).
|
|
414
|
+
if (!isDesktop) {
|
|
415
|
+
return (
|
|
416
|
+
<MobileReadingPane
|
|
417
|
+
thread={{ subject: displaySubject, messages: [] }}
|
|
418
|
+
onBack={onBack ?? (() => undefined)}
|
|
419
|
+
intelligenceOpen={mobileIntelligenceOpen}
|
|
420
|
+
onToggleIntelligence={onOpenIntelligence}
|
|
421
|
+
touchHandlers={swipeHandlers}
|
|
422
|
+
>
|
|
423
|
+
{authenticity?.dkimMismatch && (
|
|
424
|
+
<AuthenticityBanner
|
|
425
|
+
authenticity={authenticity}
|
|
426
|
+
onOpenIntelligence={onOpenIntelligence}
|
|
427
|
+
/>
|
|
428
|
+
)}
|
|
429
|
+
{renderMessages(true)}
|
|
430
|
+
{composeMode !== null && (
|
|
431
|
+
<InlineCompose
|
|
432
|
+
mode={composeMode}
|
|
433
|
+
account={activeAccount}
|
|
434
|
+
sourceMessage={lastMessageData}
|
|
435
|
+
onClose={handleCloseCompose}
|
|
436
|
+
/>
|
|
437
|
+
)}
|
|
438
|
+
</MobileReadingPane>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<article className="h-full flex flex-col">
|
|
444
|
+
{header}
|
|
445
|
+
{authenticity?.dkimMismatch && (
|
|
446
|
+
<AuthenticityBanner
|
|
447
|
+
authenticity={authenticity}
|
|
448
|
+
onOpenIntelligence={onOpenIntelligence}
|
|
449
|
+
/>
|
|
450
|
+
)}
|
|
451
|
+
<div className="flex-1 overflow-auto">{renderMessages(false)}</div>
|
|
452
|
+
{composeMode !== null && (
|
|
453
|
+
<InlineCompose
|
|
454
|
+
mode={composeMode}
|
|
455
|
+
account={activeAccount}
|
|
456
|
+
sourceMessage={lastMessageData}
|
|
457
|
+
onClose={handleCloseCompose}
|
|
458
|
+
/>
|
|
459
|
+
)}
|
|
460
|
+
</article>
|
|
461
|
+
);
|
|
462
|
+
};
|