@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,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Senders & Rules — 2-pane page: rule groups left, dense filterable
|
|
3
|
+
* sender table right. Built to scale to hundreds of flagged senders.
|
|
4
|
+
*
|
|
5
|
+
* Backend gap (#427): there is no "list all addresses with flag X" endpoint.
|
|
6
|
+
* VIP group: served from GET /me/vip-suggestions (suggested-to-promote list).
|
|
7
|
+
* Muted / Blocked groups: users search by name/email to find flagged senders
|
|
8
|
+
* via GET /addresses/search; results are filtered client-side on the flag.
|
|
9
|
+
* The real flagged-address query endpoint is tracked as a backend issue.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
addressDetailOperationsUpdateAddressMutation,
|
|
13
|
+
addressOperationsSearchAddressesOptions,
|
|
14
|
+
meOperationsListVipSuggestionsOptions,
|
|
15
|
+
meOperationsListVipSuggestionsQueryKey,
|
|
16
|
+
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
17
|
+
import type {
|
|
18
|
+
RemitImapAddressResponse,
|
|
19
|
+
RemitImapVipSuggestionEntry,
|
|
20
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
21
|
+
import {
|
|
22
|
+
Button,
|
|
23
|
+
Input,
|
|
24
|
+
Kbd,
|
|
25
|
+
SenderFlagRow,
|
|
26
|
+
SenderGroupSwitch,
|
|
27
|
+
SettingsShell,
|
|
28
|
+
} from "@remit/ui";
|
|
29
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
30
|
+
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
|
31
|
+
import { Search, Star, X } from "lucide-react";
|
|
32
|
+
import { useCallback, useEffect, useState } from "react";
|
|
33
|
+
import { ErrorState } from "@/components/ui/ErrorState";
|
|
34
|
+
import { useDebouncedValue } from "@/hooks/useDebouncedValue";
|
|
35
|
+
import { formatDate } from "@/lib/format";
|
|
36
|
+
import { SETTINGS_ID_TO_PATH, SETTINGS_NAV_ITEMS } from "@/routes/settings";
|
|
37
|
+
|
|
38
|
+
export const Route = createFileRoute("/settings/senders")({
|
|
39
|
+
component: SendersSettings,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/* ------------------------------------------------------------------ */
|
|
43
|
+
/* Types */
|
|
44
|
+
/* ------------------------------------------------------------------ */
|
|
45
|
+
|
|
46
|
+
type SenderGroup = "vip" | "muted" | "blocked";
|
|
47
|
+
|
|
48
|
+
const groupLabels: Record<SenderGroup, string> = {
|
|
49
|
+
vip: "VIPs",
|
|
50
|
+
muted: "Muted",
|
|
51
|
+
blocked: "Blocked",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/* ------------------------------------------------------------------ */
|
|
55
|
+
/* Help rail copy — matches Storybook sendersHelp exactly */
|
|
56
|
+
/* ------------------------------------------------------------------ */
|
|
57
|
+
|
|
58
|
+
const sendersHelp = (
|
|
59
|
+
<div className="space-y-3">
|
|
60
|
+
<p>
|
|
61
|
+
<strong className="text-fg">VIP</strong> senders get priority surface in
|
|
62
|
+
the daily brief and notification escalation, even when an account is
|
|
63
|
+
quiet.
|
|
64
|
+
</p>
|
|
65
|
+
<p>
|
|
66
|
+
<strong className="text-fg">Muted</strong> senders never notify and stay
|
|
67
|
+
out of the brief. Their mail still syncs and stays searchable.
|
|
68
|
+
</p>
|
|
69
|
+
<p>
|
|
70
|
+
<strong className="text-fg">Blocked</strong> senders never load images and
|
|
71
|
+
go straight to junk.
|
|
72
|
+
</p>
|
|
73
|
+
<p>
|
|
74
|
+
Tip: flag from the keyboard on any message — <Kbd>v</Kbd> VIP,{" "}
|
|
75
|
+
<Kbd>m</Kbd> mute, <Kbd>b</Kbd> block. Every rule an automation creates
|
|
76
|
+
shows up here and can be removed.
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
/* ------------------------------------------------------------------ */
|
|
82
|
+
/* VIP group: served from suggestions endpoint */
|
|
83
|
+
/* ------------------------------------------------------------------ */
|
|
84
|
+
|
|
85
|
+
function VipGroupPane({
|
|
86
|
+
query,
|
|
87
|
+
onCount,
|
|
88
|
+
}: {
|
|
89
|
+
query: string;
|
|
90
|
+
onCount?: (matched: number, total: number) => void;
|
|
91
|
+
}) {
|
|
92
|
+
const { data, isPending, isError, error } = useQuery(
|
|
93
|
+
meOperationsListVipSuggestionsOptions(),
|
|
94
|
+
);
|
|
95
|
+
const queryClient = useQueryClient();
|
|
96
|
+
|
|
97
|
+
const addMutation = useMutation({
|
|
98
|
+
...addressDetailOperationsUpdateAddressMutation(),
|
|
99
|
+
onSettled: () => {
|
|
100
|
+
queryClient.invalidateQueries({
|
|
101
|
+
queryKey: meOperationsListVipSuggestionsQueryKey(),
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const suggestions = data?.suggestions ?? [];
|
|
107
|
+
const q = query.trim().toLowerCase();
|
|
108
|
+
const filtered = q
|
|
109
|
+
? suggestions.filter(
|
|
110
|
+
(s) =>
|
|
111
|
+
(s.displayName ?? "").toLowerCase().includes(q) ||
|
|
112
|
+
s.normalizedEmail.toLowerCase().includes(q),
|
|
113
|
+
)
|
|
114
|
+
: suggestions;
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
onCount?.(filtered.length, suggestions.length);
|
|
118
|
+
}, [onCount, filtered.length, suggestions.length]);
|
|
119
|
+
|
|
120
|
+
if (isPending) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="px-row-inset py-3 text-sm text-fg-subtle animate-pulse">
|
|
123
|
+
Loading VIP suggestions…
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (isError) {
|
|
129
|
+
return (
|
|
130
|
+
<div className="px-row-inset py-3">
|
|
131
|
+
<ErrorState
|
|
132
|
+
variant="inline"
|
|
133
|
+
title="Couldn't load VIP suggestions"
|
|
134
|
+
error={error}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (suggestions.length === 0) {
|
|
141
|
+
return (
|
|
142
|
+
<div className="px-row-inset py-5 text-sm text-fg-subtle">
|
|
143
|
+
No VIP suggestions yet. As you reply to senders, they'll appear
|
|
144
|
+
here. Press <Kbd>v</Kbd> on any message to promote a sender to VIP.
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (filtered.length === 0) {
|
|
150
|
+
return (
|
|
151
|
+
<p className="px-row-inset py-5 text-sm text-fg-subtle">
|
|
152
|
+
No VIPs match “{query}”.
|
|
153
|
+
</p>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<>
|
|
159
|
+
<div className="px-row-inset py-1 text-2xs font-semibold uppercase tracking-wider text-fg-subtle">
|
|
160
|
+
Suggested — promote to VIP
|
|
161
|
+
</div>
|
|
162
|
+
{filtered.map((s: RemitImapVipSuggestionEntry) => (
|
|
163
|
+
<SenderFlagRow
|
|
164
|
+
key={s.addressId}
|
|
165
|
+
name={s.displayName ?? s.normalizedEmail}
|
|
166
|
+
email={s.normalizedEmail}
|
|
167
|
+
meta={
|
|
168
|
+
s.replyCount > 0
|
|
169
|
+
? `${s.inboundCount} received · you replied ${s.replyCount}×`
|
|
170
|
+
: `${s.inboundCount} received · you've never replied`
|
|
171
|
+
}
|
|
172
|
+
trailing={
|
|
173
|
+
<Button
|
|
174
|
+
variant="ghost"
|
|
175
|
+
size="sm"
|
|
176
|
+
icon={<Star className="size-3.5" />}
|
|
177
|
+
aria-busy={addMutation.isPending}
|
|
178
|
+
onClick={() => {
|
|
179
|
+
if (addMutation.isPending) return;
|
|
180
|
+
addMutation.mutate({
|
|
181
|
+
path: { addressId: s.addressId },
|
|
182
|
+
body: {
|
|
183
|
+
flags: {
|
|
184
|
+
vip: { value: true, setAt: Date.now(), setBy: "user" },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
Add VIP
|
|
191
|
+
</Button>
|
|
192
|
+
}
|
|
193
|
+
/>
|
|
194
|
+
))}
|
|
195
|
+
</>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* ------------------------------------------------------------------ */
|
|
200
|
+
/* Muted / Blocked groups: search-based (backend gap noted) */
|
|
201
|
+
/* ------------------------------------------------------------------ */
|
|
202
|
+
|
|
203
|
+
function SearchGroupPane({
|
|
204
|
+
group,
|
|
205
|
+
query,
|
|
206
|
+
onCount,
|
|
207
|
+
}: {
|
|
208
|
+
group: "muted" | "blocked";
|
|
209
|
+
query: string;
|
|
210
|
+
onCount?: (matched: number, total: number | null) => void;
|
|
211
|
+
}) {
|
|
212
|
+
const debouncedQ = useDebouncedValue(query.trim(), 300);
|
|
213
|
+
const queryClient = useQueryClient();
|
|
214
|
+
const [retryId, setRetryId] = useState<string | null>(null);
|
|
215
|
+
|
|
216
|
+
const enabled = debouncedQ.length >= 2;
|
|
217
|
+
|
|
218
|
+
const searchOptions = addressOperationsSearchAddressesOptions({
|
|
219
|
+
query: { q: debouncedQ, limit: 50 },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const { data, isPending, isError, error } = useQuery({
|
|
223
|
+
...searchOptions,
|
|
224
|
+
enabled,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const removeMutation = useMutation({
|
|
228
|
+
...addressDetailOperationsUpdateAddressMutation(),
|
|
229
|
+
onSuccess: () => {
|
|
230
|
+
queryClient.invalidateQueries({ queryKey: searchOptions.queryKey });
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const items = (data?.items ?? []).filter((a: RemitImapAddressResponse) =>
|
|
235
|
+
group === "muted"
|
|
236
|
+
? (a.flags?.muted?.value ?? false)
|
|
237
|
+
: (a.flags?.blocked?.value ?? false),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
// Total flagged count is unknown (no list-all-flagged endpoint), so the
|
|
242
|
+
// group rail shows the live matched-row count while a search is active.
|
|
243
|
+
onCount?.(enabled ? items.length : 0, null);
|
|
244
|
+
}, [onCount, enabled, items.length]);
|
|
245
|
+
|
|
246
|
+
if (!enabled) {
|
|
247
|
+
// Backend gap: no list-all-flagged endpoint; prompt user to search.
|
|
248
|
+
return (
|
|
249
|
+
<div className="px-row-inset py-5 text-sm text-fg-subtle space-y-2">
|
|
250
|
+
<p>
|
|
251
|
+
Type 2+ characters to search {groupLabels[group].toLowerCase()}{" "}
|
|
252
|
+
senders by name or address.
|
|
253
|
+
</p>
|
|
254
|
+
<p className="text-2xs text-fg-subtle">
|
|
255
|
+
Note: a dedicated flagged-address list endpoint is in the backlog.
|
|
256
|
+
Press <Kbd>m</Kbd> on any message to mute its sender; <Kbd>b</Kbd> to
|
|
257
|
+
block.
|
|
258
|
+
</p>
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (isPending) {
|
|
264
|
+
return (
|
|
265
|
+
<div className="px-row-inset py-3 text-sm text-fg-subtle animate-pulse">
|
|
266
|
+
Searching…
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (isError) {
|
|
272
|
+
return (
|
|
273
|
+
<div className="px-row-inset py-3">
|
|
274
|
+
<ErrorState variant="inline" title="Search failed" error={error} />
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (items.length === 0) {
|
|
280
|
+
return (
|
|
281
|
+
<p className="px-row-inset py-5 text-sm text-fg-subtle">
|
|
282
|
+
No {groupLabels[group].toLowerCase()} senders matching “
|
|
283
|
+
{debouncedQ}”.
|
|
284
|
+
</p>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<>
|
|
290
|
+
{items.map((a: RemitImapAddressResponse) => {
|
|
291
|
+
const flag = group === "muted" ? a.flags?.muted : a.flags?.blocked;
|
|
292
|
+
// The search response carries no engagement counts, so the row's
|
|
293
|
+
// info line is the flag provenance (when/why it was flagged)
|
|
294
|
+
// rather than echoing the email already in the header.
|
|
295
|
+
const meta = flag
|
|
296
|
+
? [
|
|
297
|
+
`${group}`,
|
|
298
|
+
flag.setAt
|
|
299
|
+
? formatDate(flag.setAt, {
|
|
300
|
+
month: "short",
|
|
301
|
+
year: "numeric",
|
|
302
|
+
}) || null
|
|
303
|
+
: null,
|
|
304
|
+
flag.reason ? `— ${flag.reason}` : null,
|
|
305
|
+
]
|
|
306
|
+
.filter(Boolean)
|
|
307
|
+
.join(" ")
|
|
308
|
+
: group;
|
|
309
|
+
|
|
310
|
+
const isPendingRemove =
|
|
311
|
+
removeMutation.isPending &&
|
|
312
|
+
removeMutation.variables?.path.addressId === a.addressId;
|
|
313
|
+
const isFailedRemove =
|
|
314
|
+
removeMutation.isError && retryId === a.addressId;
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<SenderFlagRow
|
|
318
|
+
key={a.addressId}
|
|
319
|
+
name={a.displayName ?? a.normalizedEmail}
|
|
320
|
+
email={a.normalizedEmail}
|
|
321
|
+
danger={group === "blocked"}
|
|
322
|
+
meta={meta}
|
|
323
|
+
trailing={
|
|
324
|
+
isFailedRemove ? (
|
|
325
|
+
<Button
|
|
326
|
+
variant="secondary"
|
|
327
|
+
size="sm"
|
|
328
|
+
onClick={() => {
|
|
329
|
+
setRetryId(a.addressId);
|
|
330
|
+
removeMutation.mutate({
|
|
331
|
+
path: { addressId: a.addressId },
|
|
332
|
+
body: { flags: { [group]: null } },
|
|
333
|
+
});
|
|
334
|
+
}}
|
|
335
|
+
>
|
|
336
|
+
Retry
|
|
337
|
+
</Button>
|
|
338
|
+
) : (
|
|
339
|
+
<Button
|
|
340
|
+
variant="ghost"
|
|
341
|
+
size="sm"
|
|
342
|
+
icon={<X className="size-3.5" />}
|
|
343
|
+
aria-busy={isPendingRemove}
|
|
344
|
+
aria-label={`Remove ${group} flag`}
|
|
345
|
+
onClick={() => {
|
|
346
|
+
if (isPendingRemove) return;
|
|
347
|
+
setRetryId(a.addressId);
|
|
348
|
+
removeMutation.mutate({
|
|
349
|
+
path: { addressId: a.addressId },
|
|
350
|
+
body: { flags: { [group]: null } },
|
|
351
|
+
});
|
|
352
|
+
}}
|
|
353
|
+
/>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
/>
|
|
357
|
+
);
|
|
358
|
+
})}
|
|
359
|
+
</>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* ------------------------------------------------------------------ */
|
|
364
|
+
/* Page component */
|
|
365
|
+
/* ------------------------------------------------------------------ */
|
|
366
|
+
|
|
367
|
+
function SendersSettings() {
|
|
368
|
+
const navigate = useNavigate();
|
|
369
|
+
const [group, setGroup] = useState<SenderGroup>("vip");
|
|
370
|
+
const [query, setQuery] = useState("");
|
|
371
|
+
const [helpOpen, setHelpOpen] = useState(true);
|
|
372
|
+
|
|
373
|
+
// Per-group counts for the rail + footer. VIP total is the suggestions
|
|
374
|
+
// list size; muted/blocked have no list endpoint, so their total is null
|
|
375
|
+
// (the rail shows the live matched-row count instead of a stale total).
|
|
376
|
+
const [matched, setMatched] = useState(0);
|
|
377
|
+
const [groupCounts, setGroupCounts] = useState<
|
|
378
|
+
Record<SenderGroup, number | null>
|
|
379
|
+
>({ vip: 0, muted: null, blocked: null });
|
|
380
|
+
|
|
381
|
+
const handleVipCount = useCallback((m: number, total: number) => {
|
|
382
|
+
setMatched(m);
|
|
383
|
+
setGroupCounts((prev) => ({ ...prev, vip: total }));
|
|
384
|
+
}, []);
|
|
385
|
+
|
|
386
|
+
const handleSearchCount = useCallback(
|
|
387
|
+
(m: number, _total: number | null) => {
|
|
388
|
+
setMatched(m);
|
|
389
|
+
setGroupCounts((prev) => ({ ...prev, [group]: m }));
|
|
390
|
+
},
|
|
391
|
+
[group],
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
const total = groupCounts[group];
|
|
395
|
+
|
|
396
|
+
const handleSelectNav = (id: string) => {
|
|
397
|
+
const path = SETTINGS_ID_TO_PATH[id];
|
|
398
|
+
if (path) void navigate({ to: path });
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<SettingsShell
|
|
403
|
+
items={SETTINGS_NAV_ITEMS}
|
|
404
|
+
activeId="senders"
|
|
405
|
+
title="Senders & Rules"
|
|
406
|
+
description="Per-sender preferences — set with one key from any message, managed here."
|
|
407
|
+
flush
|
|
408
|
+
help={sendersHelp}
|
|
409
|
+
helpOpen={helpOpen}
|
|
410
|
+
onToggleHelp={() => setHelpOpen((v) => !v)}
|
|
411
|
+
onSelect={handleSelectNav}
|
|
412
|
+
onBackToMail={() => void navigate({ to: "/mail" })}
|
|
413
|
+
>
|
|
414
|
+
<div className="flex min-h-0 flex-1 flex-col lg:flex-row">
|
|
415
|
+
<SenderGroupSwitch<SenderGroup>
|
|
416
|
+
active={group}
|
|
417
|
+
onSelect={(g) => {
|
|
418
|
+
setGroup(g);
|
|
419
|
+
setQuery("");
|
|
420
|
+
}}
|
|
421
|
+
options={(["vip", "muted", "blocked"] as SenderGroup[]).map((g) => ({
|
|
422
|
+
id: g,
|
|
423
|
+
label: groupLabels[g],
|
|
424
|
+
count: groupCounts[g],
|
|
425
|
+
}))}
|
|
426
|
+
/>
|
|
427
|
+
|
|
428
|
+
{/* Dense filterable sender table */}
|
|
429
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
430
|
+
<div className="border-b border-line px-row-inset py-2">
|
|
431
|
+
<Input
|
|
432
|
+
icon={<Search className="size-4" />}
|
|
433
|
+
placeholder={`Filter ${groupLabels[group].toLowerCase()} by name or address`}
|
|
434
|
+
className="h-8 max-w-sm"
|
|
435
|
+
value={query}
|
|
436
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<div className="min-h-0 flex-1 divide-y divide-line overflow-y-auto">
|
|
441
|
+
{group === "vip" ? (
|
|
442
|
+
<VipGroupPane query={query} onCount={handleVipCount} />
|
|
443
|
+
) : (
|
|
444
|
+
<SearchGroupPane
|
|
445
|
+
group={group}
|
|
446
|
+
query={query}
|
|
447
|
+
onCount={handleSearchCount}
|
|
448
|
+
/>
|
|
449
|
+
)}
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<footer className="flex items-center justify-between border-t border-line px-row-inset py-1 text-2xs text-fg-subtle">
|
|
453
|
+
<span>
|
|
454
|
+
{total != null
|
|
455
|
+
? `${matched} of ${total} flagged senders`
|
|
456
|
+
: `${matched} flagged senders`}
|
|
457
|
+
</span>
|
|
458
|
+
<span>
|
|
459
|
+
<Kbd>j</Kbd> <Kbd>k</Kbd> navigate · <Kbd>⌫</Kbd> remove flag
|
|
460
|
+
</span>
|
|
461
|
+
</footer>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
</SettingsShell>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suggested VIPs — now folded into Senders & Rules (VIP group).
|
|
3
|
+
* This route redirects to /settings/senders so existing deep links
|
|
4
|
+
* and smoke test paths continue to resolve gracefully.
|
|
5
|
+
*/
|
|
6
|
+
import { createFileRoute, Navigate } from "@tanstack/react-router";
|
|
7
|
+
|
|
8
|
+
export const Route = createFileRoute("/settings/suggested-vips")({
|
|
9
|
+
component: () => <Navigate to="/settings/senders" replace />,
|
|
10
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings layout route — a pass-through Outlet. Each child page
|
|
3
|
+
* (`/settings/accounts`, `/settings/senders`, …) renders its own
|
|
4
|
+
* full-viewport `SettingsShell` from @remit/ui, exactly as the
|
|
5
|
+
* approved Storybook stories do. This route only provides the route
|
|
6
|
+
* boundary; no layout chrome lives here.
|
|
7
|
+
*
|
|
8
|
+
* Shared nav items and helpers are exported so every child page imports
|
|
9
|
+
* them from this file, keeping a single source of truth for nav shape.
|
|
10
|
+
*/
|
|
11
|
+
import type { SettingsNavItem } from "@remit/ui";
|
|
12
|
+
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
|
13
|
+
import {
|
|
14
|
+
Filter,
|
|
15
|
+
FolderTree,
|
|
16
|
+
Inbox,
|
|
17
|
+
Palette,
|
|
18
|
+
Users,
|
|
19
|
+
Wrench,
|
|
20
|
+
} from "lucide-react";
|
|
21
|
+
|
|
22
|
+
/* ------------------------------------------------------------------ */
|
|
23
|
+
/* Nav items — single source of truth shared by all settings pages */
|
|
24
|
+
/* ------------------------------------------------------------------ */
|
|
25
|
+
|
|
26
|
+
export const SETTINGS_NAV_ITEMS: SettingsNavItem[] = [
|
|
27
|
+
{ id: "accounts", label: "Accounts", icon: <Inbox className="size-4" /> },
|
|
28
|
+
{
|
|
29
|
+
id: "senders",
|
|
30
|
+
label: "Senders & Rules",
|
|
31
|
+
icon: <Users className="size-4" />,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "folders",
|
|
35
|
+
label: "Folder roles",
|
|
36
|
+
icon: <FolderTree className="size-4" />,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "filters",
|
|
40
|
+
label: "Filters",
|
|
41
|
+
icon: <Filter className="size-4" />,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "appearance",
|
|
45
|
+
label: "Appearance",
|
|
46
|
+
icon: <Palette className="size-4" />,
|
|
47
|
+
},
|
|
48
|
+
{ id: "advanced", label: "Advanced", icon: <Wrench className="size-4" /> },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export const SETTINGS_ID_TO_PATH: Record<string, string> = {
|
|
52
|
+
accounts: "/settings/accounts",
|
|
53
|
+
senders: "/settings/senders",
|
|
54
|
+
folders: "/settings/folders",
|
|
55
|
+
filters: "/settings/filters",
|
|
56
|
+
appearance: "/settings/appearance",
|
|
57
|
+
advanced: "/settings/advanced",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/* ------------------------------------------------------------------ */
|
|
61
|
+
/* Route — pass-through */
|
|
62
|
+
/* ------------------------------------------------------------------ */
|
|
63
|
+
|
|
64
|
+
export const Route = createFileRoute("/settings")({
|
|
65
|
+
component: () => <Outlet />,
|
|
66
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy-specific configuration, read at runtime from `window.__REMIT_CONFIG__`
|
|
3
|
+
* (set by `/config.js`, loaded before the app bundle). One built artifact serves
|
|
4
|
+
* every deployment: the self-host image ships a default `config.js`, the AWS
|
|
5
|
+
* deploy writes one with Cognito values, and the dev server injects one from
|
|
6
|
+
* `REMIT_RUNTIME_CONFIG`. Nothing here is baked at build time.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface CognitoConfig {
|
|
10
|
+
userPoolId: string;
|
|
11
|
+
clientId: string;
|
|
12
|
+
domain: string;
|
|
13
|
+
region: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RumConfig {
|
|
17
|
+
appMonitorId: string;
|
|
18
|
+
identityPoolId: string;
|
|
19
|
+
region: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RemitRuntimeConfig {
|
|
23
|
+
apiUrl: string;
|
|
24
|
+
appOrigin: string;
|
|
25
|
+
betterAuthEnabled: boolean;
|
|
26
|
+
cognito: CognitoConfig;
|
|
27
|
+
rum: RumConfig;
|
|
28
|
+
mailboxPollIntervalSeconds?: string;
|
|
29
|
+
disableDevtools: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RawRuntimeConfig {
|
|
33
|
+
apiUrl?: string;
|
|
34
|
+
appOrigin?: string;
|
|
35
|
+
betterAuthEnabled?: boolean;
|
|
36
|
+
cognito?: Partial<CognitoConfig>;
|
|
37
|
+
rum?: Partial<RumConfig>;
|
|
38
|
+
mailboxPollIntervalSeconds?: string | number;
|
|
39
|
+
disableDevtools?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare global {
|
|
43
|
+
// eslint-disable-next-line no-var
|
|
44
|
+
var __REMIT_CONFIG__: RawRuntimeConfig | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read `window.__REMIT_CONFIG__` fresh on each call and normalize it. Reading at
|
|
49
|
+
* call time (not module load) keeps it re-evaluable: the browser sets the global
|
|
50
|
+
* once before boot, and tests swap it between cases.
|
|
51
|
+
*/
|
|
52
|
+
export const getRuntimeConfig = (): RemitRuntimeConfig => {
|
|
53
|
+
const raw: RawRuntimeConfig = globalThis.__REMIT_CONFIG__ ?? {};
|
|
54
|
+
return {
|
|
55
|
+
apiUrl: raw.apiUrl ?? "/api",
|
|
56
|
+
appOrigin: raw.appOrigin ?? "",
|
|
57
|
+
betterAuthEnabled: raw.betterAuthEnabled ?? false,
|
|
58
|
+
cognito: {
|
|
59
|
+
userPoolId: raw.cognito?.userPoolId ?? "",
|
|
60
|
+
clientId: raw.cognito?.clientId ?? "",
|
|
61
|
+
domain: raw.cognito?.domain ?? "",
|
|
62
|
+
region: raw.cognito?.region ?? "",
|
|
63
|
+
},
|
|
64
|
+
rum: {
|
|
65
|
+
appMonitorId: raw.rum?.appMonitorId ?? "",
|
|
66
|
+
identityPoolId: raw.rum?.identityPoolId ?? "",
|
|
67
|
+
region: raw.rum?.region ?? "eu-west-1",
|
|
68
|
+
},
|
|
69
|
+
mailboxPollIntervalSeconds:
|
|
70
|
+
raw.mailboxPollIntervalSeconds != null
|
|
71
|
+
? String(raw.mailboxPollIntervalSeconds)
|
|
72
|
+
: undefined,
|
|
73
|
+
disableDevtools: raw.disableDevtools ?? false,
|
|
74
|
+
};
|
|
75
|
+
};
|