@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,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OutboxPane — compound component for the outbox view (/mail/outbox route).
|
|
3
|
+
*
|
|
4
|
+
* Usage in mail.tsx:
|
|
5
|
+
*
|
|
6
|
+
* <OutboxPane>
|
|
7
|
+
* <AppShellSlotted
|
|
8
|
+
* list={<OutboxPane.List />}
|
|
9
|
+
* reading={<OutboxPane.Reading />}
|
|
10
|
+
* />
|
|
11
|
+
* </OutboxPane>
|
|
12
|
+
*
|
|
13
|
+
* On phone/tablet, use `<OutboxPane.Phone />` instead of the slot sub-views.
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
outboxDetailOperationsDeleteOutboxMessageMutation,
|
|
17
|
+
outboxDetailOperationsSendOutboxMessageMutation,
|
|
18
|
+
outboxOperationsListOutboxMessagesOptions,
|
|
19
|
+
outboxOperationsListOutboxMessagesQueryKey,
|
|
20
|
+
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
21
|
+
import type {
|
|
22
|
+
RemitImapOutboxMessageResponse,
|
|
23
|
+
RemitImapOutboxMessageStatus,
|
|
24
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
25
|
+
import { OutboxRow, ReadingPaneEmpty } from "@remit/ui";
|
|
26
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
27
|
+
import { useNavigate, useSearch } from "@tanstack/react-router";
|
|
28
|
+
import {
|
|
29
|
+
AlertCircle,
|
|
30
|
+
AlertTriangle,
|
|
31
|
+
ArrowLeft,
|
|
32
|
+
CheckCircle,
|
|
33
|
+
Clock,
|
|
34
|
+
Loader2,
|
|
35
|
+
Send,
|
|
36
|
+
} from "lucide-react";
|
|
37
|
+
import {
|
|
38
|
+
createContext,
|
|
39
|
+
type ReactNode,
|
|
40
|
+
useCallback,
|
|
41
|
+
useContext,
|
|
42
|
+
useMemo,
|
|
43
|
+
} from "react";
|
|
44
|
+
import { useCompose } from "@/components/compose/ComposeProvider";
|
|
45
|
+
import { MessageBody } from "@/components/mail/MessageBody";
|
|
46
|
+
import { NavMenuButton } from "@/components/mail/NavMenuButton";
|
|
47
|
+
import { useErrorBanners } from "@/components/ui/ErrorBannerProvider";
|
|
48
|
+
import { buildMutationErrorBanner } from "@/components/ui/error-banners";
|
|
49
|
+
import { formatDate as formatLocaleDate, toDate } from "@/lib/format";
|
|
50
|
+
import { isOutboxListRow, isUnsendableStatus } from "@/lib/outbox-status";
|
|
51
|
+
import { cn } from "@/lib/utils";
|
|
52
|
+
|
|
53
|
+
/* ------------------------------------------------------------------ */
|
|
54
|
+
/* Helpers (shared between List, Reading, Phone sub-views) */
|
|
55
|
+
/* ------------------------------------------------------------------ */
|
|
56
|
+
|
|
57
|
+
const STATUS_CONFIG: Record<
|
|
58
|
+
Exclude<RemitImapOutboxMessageStatus, "draft">,
|
|
59
|
+
{ icon: typeof Send; label: string; className: string }
|
|
60
|
+
> = {
|
|
61
|
+
queued: {
|
|
62
|
+
icon: Clock,
|
|
63
|
+
label: "Queued",
|
|
64
|
+
className: "text-warning",
|
|
65
|
+
},
|
|
66
|
+
sending: {
|
|
67
|
+
icon: Loader2,
|
|
68
|
+
label: "Sending…",
|
|
69
|
+
className: "text-accent-2",
|
|
70
|
+
},
|
|
71
|
+
sent: {
|
|
72
|
+
icon: CheckCircle,
|
|
73
|
+
label: "Sent",
|
|
74
|
+
className: "text-positive",
|
|
75
|
+
},
|
|
76
|
+
failed: {
|
|
77
|
+
icon: AlertCircle,
|
|
78
|
+
label: "Failed",
|
|
79
|
+
className: "text-danger",
|
|
80
|
+
},
|
|
81
|
+
blocked: {
|
|
82
|
+
icon: AlertTriangle,
|
|
83
|
+
label: "Blocked",
|
|
84
|
+
className: "text-warning",
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const formatDate = (timestamp: number): string => {
|
|
89
|
+
const date = toDate(timestamp);
|
|
90
|
+
const isToday = date.toDateString() === new Date().toDateString();
|
|
91
|
+
return isToday
|
|
92
|
+
? formatLocaleDate(timestamp, { hour: "numeric", minute: "2-digit" })
|
|
93
|
+
: formatLocaleDate(timestamp, { month: "short", day: "numeric" });
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const formatDateFull = (timestamp: number): string =>
|
|
97
|
+
formatLocaleDate(timestamp, { dateStyle: "medium", timeStyle: "short" });
|
|
98
|
+
|
|
99
|
+
const formatRecipients = (message: RemitImapOutboxMessageResponse): string => {
|
|
100
|
+
const recipients = message.toAddresses ?? [];
|
|
101
|
+
if (recipients.length === 0) return "No recipients";
|
|
102
|
+
if (recipients.length === 1) return recipients[0];
|
|
103
|
+
return `${recipients[0]} +${recipients.length - 1}`;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/* ------------------------------------------------------------------ */
|
|
107
|
+
/* Context */
|
|
108
|
+
/* ------------------------------------------------------------------ */
|
|
109
|
+
|
|
110
|
+
interface OutboxPaneContextValue {
|
|
111
|
+
messages: RemitImapOutboxMessageResponse[];
|
|
112
|
+
isLoading: boolean;
|
|
113
|
+
selectedMessageId: string | undefined;
|
|
114
|
+
selectedMessage: RemitImapOutboxMessageResponse | undefined;
|
|
115
|
+
onSelectMessage: (id: string | undefined) => void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const OutboxPaneCtx = createContext<OutboxPaneContextValue | null>(null);
|
|
119
|
+
|
|
120
|
+
function useOutboxPane(): OutboxPaneContextValue {
|
|
121
|
+
const ctx = useContext(OutboxPaneCtx);
|
|
122
|
+
if (!ctx) throw new Error("OutboxPane.* must be used inside <OutboxPane>");
|
|
123
|
+
return ctx;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ------------------------------------------------------------------ */
|
|
127
|
+
/* Provider */
|
|
128
|
+
/* ------------------------------------------------------------------ */
|
|
129
|
+
|
|
130
|
+
interface OutboxPaneProps {
|
|
131
|
+
children: ReactNode;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function OutboxPaneProvider({ children }: OutboxPaneProps) {
|
|
135
|
+
const navigate = useNavigate();
|
|
136
|
+
// Read selectedOutboxMessageId from the URL directly — this avoids threading
|
|
137
|
+
// a prop through the parent shell for a param that only outbox uses.
|
|
138
|
+
const { selectedOutboxMessageId: selectedMessageId } = useSearch({
|
|
139
|
+
strict: false,
|
|
140
|
+
}) as { selectedOutboxMessageId?: string };
|
|
141
|
+
|
|
142
|
+
const { data: outboxResponse, isLoading } = useQuery(
|
|
143
|
+
outboxOperationsListOutboxMessagesOptions(),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Outbox surfaces in-flight + actionable rows only. `draft` lives in the
|
|
147
|
+
// Drafts view; `sent` rows are deleted by the IMAP append handler once
|
|
148
|
+
// the message lands in Sent (issue #178), but we filter defensively so a
|
|
149
|
+
// successfully-sent row never lingers in the Outbox list even if APPEND
|
|
150
|
+
// has not completed yet (issue #193).
|
|
151
|
+
const messages = useMemo(
|
|
152
|
+
() =>
|
|
153
|
+
(outboxResponse?.items ?? []).filter((item) =>
|
|
154
|
+
isOutboxListRow(item.status),
|
|
155
|
+
),
|
|
156
|
+
[outboxResponse],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const selectedMessage = useMemo(
|
|
160
|
+
() => messages.find((m) => m.outboxMessageId === selectedMessageId),
|
|
161
|
+
[messages, selectedMessageId],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const handleSelectMessage = useCallback(
|
|
165
|
+
(id: string | undefined) => {
|
|
166
|
+
navigate({
|
|
167
|
+
to: "/mail/outbox",
|
|
168
|
+
search: (prev) => ({
|
|
169
|
+
...prev,
|
|
170
|
+
selectedOutboxMessageId: id,
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
[navigate],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const ctx: OutboxPaneContextValue = {
|
|
178
|
+
messages,
|
|
179
|
+
isLoading,
|
|
180
|
+
selectedMessageId,
|
|
181
|
+
selectedMessage,
|
|
182
|
+
onSelectMessage: handleSelectMessage,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<OutboxPaneCtx.Provider value={ctx}>{children}</OutboxPaneCtx.Provider>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ------------------------------------------------------------------ */
|
|
191
|
+
/* Row component */
|
|
192
|
+
/* ------------------------------------------------------------------ */
|
|
193
|
+
|
|
194
|
+
interface OutboxMessageRowProps {
|
|
195
|
+
message: RemitImapOutboxMessageResponse;
|
|
196
|
+
isSelected: boolean;
|
|
197
|
+
onSelect: () => void;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function OutboxMessageRow({
|
|
201
|
+
message,
|
|
202
|
+
isSelected,
|
|
203
|
+
onSelect,
|
|
204
|
+
}: OutboxMessageRowProps) {
|
|
205
|
+
const queryClient = useQueryClient();
|
|
206
|
+
const { openCompose } = useCompose();
|
|
207
|
+
const { pushError } = useErrorBanners();
|
|
208
|
+
|
|
209
|
+
const invalidateOutbox = useCallback(() => {
|
|
210
|
+
queryClient.invalidateQueries({
|
|
211
|
+
queryKey: outboxOperationsListOutboxMessagesQueryKey(),
|
|
212
|
+
});
|
|
213
|
+
}, [queryClient]);
|
|
214
|
+
|
|
215
|
+
const retryMutation = useMutation({
|
|
216
|
+
...outboxDetailOperationsSendOutboxMessageMutation(),
|
|
217
|
+
onSuccess: invalidateOutbox,
|
|
218
|
+
onError: (error) => {
|
|
219
|
+
// A failed retry must not look successful — surface it. A fatal 5xx
|
|
220
|
+
// also escalates through the global MutationCache.onError sink.
|
|
221
|
+
pushError(
|
|
222
|
+
buildMutationErrorBanner(
|
|
223
|
+
"Couldn't resend message",
|
|
224
|
+
"The message wasn't sent.",
|
|
225
|
+
error,
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const deleteMutation = useMutation({
|
|
232
|
+
...outboxDetailOperationsDeleteOutboxMessageMutation(),
|
|
233
|
+
onSuccess: invalidateOutbox,
|
|
234
|
+
onError: (error) => {
|
|
235
|
+
pushError(
|
|
236
|
+
buildMutationErrorBanner(
|
|
237
|
+
"Couldn't delete message",
|
|
238
|
+
"The message wasn't deleted.",
|
|
239
|
+
error,
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const status = message.status as Exclude<
|
|
246
|
+
RemitImapOutboxMessageStatus,
|
|
247
|
+
"draft"
|
|
248
|
+
>;
|
|
249
|
+
if (!STATUS_CONFIG[status]) return null;
|
|
250
|
+
|
|
251
|
+
const showError = isUnsendableStatus(message.status);
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<OutboxRow
|
|
255
|
+
recipients={formatRecipients(message)}
|
|
256
|
+
subject={message.subject || ""}
|
|
257
|
+
time={formatDate(message.sentAt ?? message.updatedAt)}
|
|
258
|
+
status={status}
|
|
259
|
+
error={showError ? (message.lastError ?? undefined) : undefined}
|
|
260
|
+
selected={isSelected}
|
|
261
|
+
onSelect={onSelect}
|
|
262
|
+
onRetry={
|
|
263
|
+
status === "failed"
|
|
264
|
+
? () =>
|
|
265
|
+
retryMutation.mutate({
|
|
266
|
+
path: { outboxMessageId: message.outboxMessageId },
|
|
267
|
+
})
|
|
268
|
+
: undefined
|
|
269
|
+
}
|
|
270
|
+
retrying={retryMutation.isPending}
|
|
271
|
+
onEdit={() =>
|
|
272
|
+
openCompose({
|
|
273
|
+
mode: "new",
|
|
274
|
+
outboxMessageId: message.outboxMessageId,
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
onDelete={() =>
|
|
278
|
+
deleteMutation.mutate({
|
|
279
|
+
path: { outboxMessageId: message.outboxMessageId },
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
deleting={deleteMutation.isPending}
|
|
283
|
+
/>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ------------------------------------------------------------------ */
|
|
288
|
+
/* Detail component */
|
|
289
|
+
/* ------------------------------------------------------------------ */
|
|
290
|
+
|
|
291
|
+
interface OutboxMessageDetailProps {
|
|
292
|
+
message: RemitImapOutboxMessageResponse;
|
|
293
|
+
onBack?: () => void;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function OutboxMessageDetail({ message, onBack }: OutboxMessageDetailProps) {
|
|
297
|
+
const status = message.status as Exclude<
|
|
298
|
+
RemitImapOutboxMessageStatus,
|
|
299
|
+
"draft"
|
|
300
|
+
>;
|
|
301
|
+
const config = STATUS_CONFIG[status];
|
|
302
|
+
const StatusIcon = config?.icon ?? Send;
|
|
303
|
+
const sentLabel = message.sentAt
|
|
304
|
+
? formatDateFull(message.sentAt)
|
|
305
|
+
: formatDateFull(message.updatedAt);
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div className="h-full overflow-y-auto bg-canvas">
|
|
309
|
+
<div className="border-b border-line p-4">
|
|
310
|
+
{onBack && (
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
onClick={onBack}
|
|
314
|
+
className="mb-3 inline-flex items-center gap-1 text-sm text-fg-muted hover:text-fg"
|
|
315
|
+
>
|
|
316
|
+
<ArrowLeft className="size-4" />
|
|
317
|
+
<span>Back</span>
|
|
318
|
+
</button>
|
|
319
|
+
)}
|
|
320
|
+
<h1 className="text-xl font-semibold mb-3">
|
|
321
|
+
{message.subject || "(No subject)"}
|
|
322
|
+
</h1>
|
|
323
|
+
<div className="space-y-1 text-sm">
|
|
324
|
+
<div className="flex gap-2">
|
|
325
|
+
<span className="text-fg-muted shrink-0 w-12">From:</span>
|
|
326
|
+
<span className="text-fg">
|
|
327
|
+
{message.fromName
|
|
328
|
+
? `${message.fromName} <${message.fromAddress}>`
|
|
329
|
+
: message.fromAddress}
|
|
330
|
+
</span>
|
|
331
|
+
</div>
|
|
332
|
+
<div className="flex gap-2">
|
|
333
|
+
<span className="text-fg-muted shrink-0 w-12">To:</span>
|
|
334
|
+
<span className="text-fg">
|
|
335
|
+
{(message.toAddresses ?? []).join(", ")}
|
|
336
|
+
</span>
|
|
337
|
+
</div>
|
|
338
|
+
{message.ccAddresses && message.ccAddresses.length > 0 && (
|
|
339
|
+
<div className="flex gap-2">
|
|
340
|
+
<span className="text-fg-muted shrink-0 w-12">Cc:</span>
|
|
341
|
+
<span className="text-fg">{message.ccAddresses.join(", ")}</span>
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
<div className="flex gap-2">
|
|
345
|
+
<span className="text-fg-muted shrink-0 w-12">Date:</span>
|
|
346
|
+
<span className="text-fg">{sentLabel}</span>
|
|
347
|
+
</div>
|
|
348
|
+
{config && (
|
|
349
|
+
<div className="flex items-center gap-2 pt-2">
|
|
350
|
+
<StatusIcon
|
|
351
|
+
className={cn(
|
|
352
|
+
"size-4",
|
|
353
|
+
config.className,
|
|
354
|
+
status === "sending" && "animate-spin",
|
|
355
|
+
)}
|
|
356
|
+
/>
|
|
357
|
+
<span className={cn("text-xs font-medium", config.className)}>
|
|
358
|
+
{config.label}
|
|
359
|
+
</span>
|
|
360
|
+
{isUnsendableStatus(message.status) && message.lastError && (
|
|
361
|
+
<span className="text-xs text-fg-muted">
|
|
362
|
+
— {message.lastError}
|
|
363
|
+
</span>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
)}
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
<div className="p-4">
|
|
370
|
+
<MessageBody html={message.htmlBody} text={message.textBody} />
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* ------------------------------------------------------------------ */
|
|
377
|
+
/* Sub-views */
|
|
378
|
+
/* ------------------------------------------------------------------ */
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Outbox list with datum bar header. Mount in the `list` slot of `AppShellSlotted`.
|
|
382
|
+
*/
|
|
383
|
+
function OutboxList() {
|
|
384
|
+
const { messages, isLoading, selectedMessageId, onSelectMessage } =
|
|
385
|
+
useOutboxPane();
|
|
386
|
+
|
|
387
|
+
if (isLoading) {
|
|
388
|
+
return (
|
|
389
|
+
<div className="flex h-full items-center justify-center bg-canvas">
|
|
390
|
+
<span className="text-fg-muted">Loading...</span>
|
|
391
|
+
</div>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (messages.length === 0) {
|
|
396
|
+
return (
|
|
397
|
+
<div className="flex h-full bg-surface">
|
|
398
|
+
<ReadingPaneEmpty message="No outbox messages" showHints={false} />
|
|
399
|
+
</div>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
<div className="h-full flex flex-col bg-canvas">
|
|
405
|
+
{/* List datum bar (40px, the shared `--spacing-pane-header`): keeps
|
|
406
|
+
the bottom hairline on the same y as the mailbox view so the grid
|
|
407
|
+
line stays continuous across nav → outbox (no staircase, #422). */}
|
|
408
|
+
<header className="flex h-pane-header shrink-0 items-center gap-2 border-b border-line px-row-inset">
|
|
409
|
+
<NavMenuButton />
|
|
410
|
+
<h1 className="min-w-0 flex-1 truncate text-sm font-semibold text-fg">
|
|
411
|
+
Outbox
|
|
412
|
+
</h1>
|
|
413
|
+
<span className="shrink-0 text-2xs text-fg-subtle">
|
|
414
|
+
{messages.length} {messages.length === 1 ? "message" : "messages"}
|
|
415
|
+
</span>
|
|
416
|
+
</header>
|
|
417
|
+
<div className="flex-1 overflow-y-auto">
|
|
418
|
+
{messages.map((message) => (
|
|
419
|
+
<OutboxMessageRow
|
|
420
|
+
key={message.outboxMessageId}
|
|
421
|
+
message={message}
|
|
422
|
+
isSelected={selectedMessageId === message.outboxMessageId}
|
|
423
|
+
onSelect={() => onSelectMessage(message.outboxMessageId)}
|
|
424
|
+
/>
|
|
425
|
+
))}
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Outbox reading pane. Mount in the `reading` slot of `AppShellSlotted`.
|
|
433
|
+
* Only rendered ≥ 1024px.
|
|
434
|
+
*/
|
|
435
|
+
function OutboxReading() {
|
|
436
|
+
const { selectedMessage } = useOutboxPane();
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<section className="flex h-full w-full min-w-0 flex-col bg-canvas">
|
|
440
|
+
{/* Detail datum bar (40px): outbox has no message-action toolbar,
|
|
441
|
+
but the datum bar must be present so its bottom hairline lines
|
|
442
|
+
up with the list pane's, keeping the grid continuous (#422). */}
|
|
443
|
+
<header className="flex h-pane-header shrink-0 items-center border-b border-line px-3" />
|
|
444
|
+
<div className="min-h-0 flex-1 overflow-hidden">
|
|
445
|
+
{selectedMessage ? (
|
|
446
|
+
<OutboxMessageDetail message={selectedMessage} />
|
|
447
|
+
) : (
|
|
448
|
+
<ReadingPaneEmpty
|
|
449
|
+
message="Select a message to read"
|
|
450
|
+
showHints={false}
|
|
451
|
+
/>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
</section>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Phone view: detail when a message is selected, or the list.
|
|
460
|
+
* Mount in the `list` slot of `AppShellSlotted` on single-pane tiers.
|
|
461
|
+
*/
|
|
462
|
+
function OutboxPhone() {
|
|
463
|
+
const {
|
|
464
|
+
messages,
|
|
465
|
+
isLoading,
|
|
466
|
+
selectedMessageId,
|
|
467
|
+
selectedMessage,
|
|
468
|
+
onSelectMessage,
|
|
469
|
+
} = useOutboxPane();
|
|
470
|
+
|
|
471
|
+
if (isLoading) {
|
|
472
|
+
return (
|
|
473
|
+
<div className="flex h-full items-center justify-center bg-canvas">
|
|
474
|
+
<span className="text-fg-muted">Loading...</span>
|
|
475
|
+
</div>
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (selectedMessage) {
|
|
480
|
+
return (
|
|
481
|
+
<OutboxMessageDetail
|
|
482
|
+
message={selectedMessage}
|
|
483
|
+
onBack={() => onSelectMessage(undefined)}
|
|
484
|
+
/>
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (messages.length === 0) {
|
|
489
|
+
return (
|
|
490
|
+
<div className="flex h-full bg-surface">
|
|
491
|
+
<ReadingPaneEmpty message="No outbox messages" showHints={false} />
|
|
492
|
+
</div>
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return (
|
|
497
|
+
<div className="h-full flex flex-col bg-canvas">
|
|
498
|
+
<header className="flex h-pane-header shrink-0 items-center gap-2 border-b border-line px-row-inset">
|
|
499
|
+
<NavMenuButton />
|
|
500
|
+
<h1 className="min-w-0 flex-1 truncate text-sm font-semibold text-fg">
|
|
501
|
+
Outbox
|
|
502
|
+
</h1>
|
|
503
|
+
<span className="shrink-0 text-2xs text-fg-subtle">
|
|
504
|
+
{messages.length} {messages.length === 1 ? "message" : "messages"}
|
|
505
|
+
</span>
|
|
506
|
+
</header>
|
|
507
|
+
<div className="flex-1 overflow-y-auto">
|
|
508
|
+
{messages.map((message) => (
|
|
509
|
+
<OutboxMessageRow
|
|
510
|
+
key={message.outboxMessageId}
|
|
511
|
+
message={message}
|
|
512
|
+
isSelected={selectedMessageId === message.outboxMessageId}
|
|
513
|
+
onSelect={() => onSelectMessage(message.outboxMessageId)}
|
|
514
|
+
/>
|
|
515
|
+
))}
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/* ------------------------------------------------------------------ */
|
|
522
|
+
/* Compound component assembly */
|
|
523
|
+
/* ------------------------------------------------------------------ */
|
|
524
|
+
|
|
525
|
+
const OutboxPane = Object.assign(OutboxPaneProvider, {
|
|
526
|
+
List: OutboxList,
|
|
527
|
+
Reading: OutboxReading,
|
|
528
|
+
Phone: OutboxPhone,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
export { OutboxPane };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { PullToRefresh as KitPullToRefresh } from "@remit/ui";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
|
|
4
|
+
import { useTriggerSync } from "@/hooks/useTriggerSync";
|
|
5
|
+
|
|
6
|
+
interface PullToRefreshProps {
|
|
7
|
+
accountId: string;
|
|
8
|
+
children: ReactElement;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Binds the kit pull-to-refresh gesture to a mailbox sync for the active
|
|
13
|
+
* account: a pull triggers the sync and the in-flight state suspends the
|
|
14
|
+
* gesture until it settles.
|
|
15
|
+
*/
|
|
16
|
+
export const PullToRefresh = ({ accountId, children }: PullToRefreshProps) => {
|
|
17
|
+
const { triggerAsync, isPending } = useTriggerSync(accountId);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<KitPullToRefresh onRefresh={triggerAsync} isRefreshing={isPending}>
|
|
21
|
+
{children}
|
|
22
|
+
</KitPullToRefresh>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { messageOperationsGetRawMessageOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { ErrorState } from "@/components/ui/ErrorState";
|
|
4
|
+
|
|
5
|
+
interface RawMessageViewProps {
|
|
6
|
+
/** The message whose raw RFC822/MIME source to fetch and display. */
|
|
7
|
+
messageId: string;
|
|
8
|
+
/**
|
|
9
|
+
* Only fetch when the raw view is actually visible. The expanded card
|
|
10
|
+
* mounts this lazily, but gating on `enabled` keeps the request from
|
|
11
|
+
* firing if a parent ever renders it ahead of time.
|
|
12
|
+
*/
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const LoadingSkeleton = () => (
|
|
17
|
+
// biome-ignore lint/a11y/useSemanticElements: <div> with role="status" preserves block layout; <output> is inline
|
|
18
|
+
<div
|
|
19
|
+
className="animate-pulse space-y-2"
|
|
20
|
+
role="status"
|
|
21
|
+
aria-label="Loading raw email"
|
|
22
|
+
>
|
|
23
|
+
<div className="h-4 bg-surface-sunken rounded w-full" />
|
|
24
|
+
<div className="h-4 bg-surface-sunken rounded w-11/12" />
|
|
25
|
+
<div className="h-4 bg-surface-sunken rounded w-5/6" />
|
|
26
|
+
<div className="h-4 bg-surface-sunken rounded w-3/4" />
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Render the raw RFC822/MIME source (headers + body) of a message in a
|
|
32
|
+
* scrollable monospace block. Distinguishes loading / failed / loaded so an
|
|
33
|
+
* empty raw body is never confused with a fetch failure
|
|
34
|
+
* (memory: feedback_never_hide_failure).
|
|
35
|
+
*/
|
|
36
|
+
export const RawMessageView = ({
|
|
37
|
+
messageId,
|
|
38
|
+
enabled = true,
|
|
39
|
+
}: RawMessageViewProps) => {
|
|
40
|
+
const { data, isLoading, isError, error, refetch } = useQuery({
|
|
41
|
+
...messageOperationsGetRawMessageOptions({ path: { messageId } }),
|
|
42
|
+
enabled,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (isLoading) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="message-body">
|
|
48
|
+
<LoadingSkeleton />
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (isError) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="message-body">
|
|
56
|
+
<ErrorState
|
|
57
|
+
variant="inline"
|
|
58
|
+
title="Couldn't load the raw email"
|
|
59
|
+
error={error}
|
|
60
|
+
onRetry={() => refetch()}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const raw = data?.raw ?? "";
|
|
67
|
+
if (raw === "") {
|
|
68
|
+
return (
|
|
69
|
+
<div className="message-body">
|
|
70
|
+
<p className="text-fg-muted text-sm italic">
|
|
71
|
+
No raw source available for this message.
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="message-body">
|
|
79
|
+
<pre className="email-text whitespace-pre-wrap break-words text-sm leading-relaxed bg-surface-sunken rounded-md p-3 max-h-[32rem] overflow-auto">
|
|
80
|
+
{raw}
|
|
81
|
+
</pre>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render invariants for the shared reading-pane zero-state and the keyboard
|
|
3
|
+
* hint bar (#785). Both ship as remit-ui components consumed by the live mail
|
|
4
|
+
* routes. Uses `react-dom/server` so no jsdom is needed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { describe, it } from "node:test";
|
|
9
|
+
import { KeyboardHintBar, ReadingPaneEmpty } from "@remit/ui";
|
|
10
|
+
import React, { createElement } from "react";
|
|
11
|
+
import { renderToString } from "react-dom/server";
|
|
12
|
+
|
|
13
|
+
// See MessageToolbar.render.test.ts: the SSR test loader transpiles remit-ui
|
|
14
|
+
// `.tsx` with the classic JSX runtime, which needs a global `React`.
|
|
15
|
+
(globalThis as { React?: typeof React }).React = React;
|
|
16
|
+
|
|
17
|
+
describe("ReadingPaneEmpty (#785)", () => {
|
|
18
|
+
it("renders the prompt and the j/k/Enter Kbd hints by default", () => {
|
|
19
|
+
const html = renderToString(createElement(ReadingPaneEmpty) as never);
|
|
20
|
+
assert.match(html, /Select a thread to read/);
|
|
21
|
+
assert.match(html, /<kbd[^>]*>j<\/kbd>/);
|
|
22
|
+
assert.match(html, /<kbd[^>]*>k<\/kbd>/);
|
|
23
|
+
assert.match(html, /<kbd[^>]*>Enter<\/kbd>/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("can hide the hint line on touch surfaces", () => {
|
|
27
|
+
const html = renderToString(
|
|
28
|
+
createElement(ReadingPaneEmpty, { showHints: false }) as never,
|
|
29
|
+
);
|
|
30
|
+
assert.equal(/<kbd/.test(html), false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("KeyboardHintBar (#785)", () => {
|
|
35
|
+
it("renders the default hint set", () => {
|
|
36
|
+
const html = renderToString(createElement(KeyboardHintBar) as never);
|
|
37
|
+
assert.match(html, /navigate/);
|
|
38
|
+
assert.match(html, /mute/);
|
|
39
|
+
assert.match(html, /all shortcuts/);
|
|
40
|
+
assert.match(html, /<kbd[^>]*>j<\/kbd>/);
|
|
41
|
+
});
|
|
42
|
+
});
|