@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,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MailListHeader — the header-only list-pane shell shared by every list view.
|
|
3
|
+
*
|
|
4
|
+
* Composes the kit `MailHeader` (hamburger + title + unread + search) and the
|
|
5
|
+
* `<section>` column shell with an optional pinned footer. It owns no filter
|
|
6
|
+
* surface: the daily brief lets the kit `BriefSections` own the filter row,
|
|
7
|
+
* while `MailViewChrome` slots a `FilterSheet` into the body for the inbox /
|
|
8
|
+
* flagged views.
|
|
9
|
+
*
|
|
10
|
+
* Search comes from `MailContext` (one source of truth, mirrored to the URL).
|
|
11
|
+
* On phone the magnifier opens a full-screen `MobileSearchView` takeover instead
|
|
12
|
+
* of expanding over the title; tablet and desktop keep the inline header search
|
|
13
|
+
* and, once a query is present, swap the list-pane body to the same kit
|
|
14
|
+
* `SearchResults` sections under the shared `FilterSheet`. Consumers feed the
|
|
15
|
+
* filter chrome and query-narrowed results; both tiers render identical rows.
|
|
16
|
+
*
|
|
17
|
+
* Results split into two sections: "Top matches" (literal/instant search) and
|
|
18
|
+
* "Related" (semantic). The consumer dedupes them — a thread in both appears
|
|
19
|
+
* only under "Top matches". Each loads independently; an empty section drops out
|
|
20
|
+
* kit-side, so a "Related"-only result still shows when the literal search finds
|
|
21
|
+
* nothing. The hamburger opens the nav drawer via the enclosing `AppShellSlotted`.
|
|
22
|
+
*/
|
|
23
|
+
import {
|
|
24
|
+
FilterSheet,
|
|
25
|
+
type FilterSheetProps,
|
|
26
|
+
MailHeader,
|
|
27
|
+
MobileSearchView,
|
|
28
|
+
type SearchResult,
|
|
29
|
+
type SearchResultSection,
|
|
30
|
+
SearchResults,
|
|
31
|
+
useAppShellLayout,
|
|
32
|
+
} from "@remit/ui";
|
|
33
|
+
import { type ReactNode, useState } from "react";
|
|
34
|
+
import { useLayoutTier } from "@/hooks/useLayoutTier";
|
|
35
|
+
import { useMailContext } from "@/lib/mail-context";
|
|
36
|
+
import { loadRecentSearches, saveRecentSearch } from "@/lib/recent-searches";
|
|
37
|
+
import {
|
|
38
|
+
parseSearchTokens,
|
|
39
|
+
removeSearchToken,
|
|
40
|
+
searchTokenLabel,
|
|
41
|
+
} from "@/lib/search-tokens";
|
|
42
|
+
|
|
43
|
+
interface MailListHeaderProps {
|
|
44
|
+
title: string;
|
|
45
|
+
unreadCount: number;
|
|
46
|
+
/** The list body (filter sheet / sections / virtualized rows). */
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
/** Pinned below the scrollable list (e.g. the keyboard hint bar). */
|
|
49
|
+
footer?: ReactNode;
|
|
50
|
+
/** Filter chrome for the phone search takeover. Omit to drop the filter row. */
|
|
51
|
+
searchFilter?: Omit<FilterSheetProps, "children">;
|
|
52
|
+
/** Literal/instant results — the "Top matches" section. */
|
|
53
|
+
searchResults?: SearchResult[];
|
|
54
|
+
searchLoading?: boolean;
|
|
55
|
+
/** Semantic results — the "Related" section, deduped against "Top matches". */
|
|
56
|
+
relatedResults?: SearchResult[];
|
|
57
|
+
relatedLoading?: boolean;
|
|
58
|
+
onSelectSearchResult?: (result: SearchResult) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function MailListHeader({
|
|
62
|
+
title,
|
|
63
|
+
unreadCount,
|
|
64
|
+
children,
|
|
65
|
+
footer,
|
|
66
|
+
searchFilter,
|
|
67
|
+
searchResults,
|
|
68
|
+
searchLoading,
|
|
69
|
+
relatedResults,
|
|
70
|
+
relatedLoading,
|
|
71
|
+
onSelectSearchResult,
|
|
72
|
+
}: MailListHeaderProps) {
|
|
73
|
+
const {
|
|
74
|
+
searchInput,
|
|
75
|
+
onSearchChange,
|
|
76
|
+
onSearchClear,
|
|
77
|
+
mailboxNameIndex,
|
|
78
|
+
accountNameIndex,
|
|
79
|
+
} = useMailContext();
|
|
80
|
+
const layout = useAppShellLayout();
|
|
81
|
+
const tier = useLayoutTier();
|
|
82
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
83
|
+
const [recentSearches, setRecentSearches] = useState(loadRecentSearches);
|
|
84
|
+
|
|
85
|
+
const hasQuery = searchInput.trim().length > 0;
|
|
86
|
+
// Filter tokens (`from:`, `has:attachment`, `in:`, `account:`, …) parsed live
|
|
87
|
+
// from the typed query render as removable chips above the sections;
|
|
88
|
+
// removing one edits the query text directly, which re-parses on the next
|
|
89
|
+
// render. `in:`/`account:` resolve against the shared name indexes so a
|
|
90
|
+
// chip only appears for a name that actually matches a mailbox/account.
|
|
91
|
+
const tokenChips = parseSearchTokens(searchInput, {
|
|
92
|
+
mailboxesByName: mailboxNameIndex,
|
|
93
|
+
accountsByName: accountNameIndex,
|
|
94
|
+
}).tokens.map((token) => ({
|
|
95
|
+
label: searchTokenLabel(token),
|
|
96
|
+
onRemove: () => onSearchChange(removeSearchToken(searchInput, token)),
|
|
97
|
+
}));
|
|
98
|
+
const topMatches = searchResults ?? [];
|
|
99
|
+
const related = relatedResults ?? [];
|
|
100
|
+
// Always offer both sections while a query is present; the kit drops the empty
|
|
101
|
+
// ones, so a "Related"-only hit still shows and two empties fall to its empty
|
|
102
|
+
// state. The empty-query case (recent searches) is the kit's job.
|
|
103
|
+
const sections: SearchResultSection[] = hasQuery
|
|
104
|
+
? [
|
|
105
|
+
{ id: "top", label: "Top matches", results: topMatches },
|
|
106
|
+
{ id: "related", label: "Related", results: related },
|
|
107
|
+
]
|
|
108
|
+
: [];
|
|
109
|
+
// Skeleton only while nothing is in yet — once either section has rows, show
|
|
110
|
+
// them and let the other arrive (or not). Keeps the two sources independent.
|
|
111
|
+
const hasAnyResult = topMatches.length + related.length > 0;
|
|
112
|
+
const resultsLoading =
|
|
113
|
+
!hasAnyResult && (searchLoading === true || relatedLoading === true);
|
|
114
|
+
|
|
115
|
+
if (tier === "phone" && searchOpen) {
|
|
116
|
+
const handleSelectResult = (result: SearchResult) => {
|
|
117
|
+
setRecentSearches(saveRecentSearch(searchInput));
|
|
118
|
+
setSearchOpen(false);
|
|
119
|
+
onSelectSearchResult?.(result);
|
|
120
|
+
};
|
|
121
|
+
return (
|
|
122
|
+
<MobileSearchView
|
|
123
|
+
value={searchInput}
|
|
124
|
+
onChange={onSearchChange}
|
|
125
|
+
onClear={onSearchClear}
|
|
126
|
+
onCancel={() => {
|
|
127
|
+
setSearchOpen(false);
|
|
128
|
+
onSearchClear();
|
|
129
|
+
}}
|
|
130
|
+
filter={searchFilter}
|
|
131
|
+
recentSearches={recentSearches}
|
|
132
|
+
onPickRecent={onSearchChange}
|
|
133
|
+
sections={sections}
|
|
134
|
+
loading={resultsLoading}
|
|
135
|
+
onSelectResult={handleSelectResult}
|
|
136
|
+
tokens={tokenChips}
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Tablet + desktop keep the inline toolbar search; once a query is present the
|
|
142
|
+
// list-pane body swaps to the same sectioned results the phone takeover shows,
|
|
143
|
+
// under the same FilterSheet. Clearing the query restores the normal list.
|
|
144
|
+
const showInlineResults = tier !== "phone" && hasQuery;
|
|
145
|
+
const handleSelectInlineResult = (result: SearchResult) => {
|
|
146
|
+
setRecentSearches(saveRecentSearch(searchInput));
|
|
147
|
+
onSelectSearchResult?.(result);
|
|
148
|
+
};
|
|
149
|
+
const results = (
|
|
150
|
+
<SearchResults
|
|
151
|
+
value={searchInput}
|
|
152
|
+
sections={sections}
|
|
153
|
+
loading={resultsLoading}
|
|
154
|
+
onSelectResult={handleSelectInlineResult}
|
|
155
|
+
tokens={tokenChips}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
const body = !showInlineResults ? (
|
|
159
|
+
children
|
|
160
|
+
) : searchFilter ? (
|
|
161
|
+
<FilterSheet {...searchFilter}>{results}</FilterSheet>
|
|
162
|
+
) : (
|
|
163
|
+
<div className="h-full overflow-y-auto">{results}</div>
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<section className="flex h-full w-full flex-col bg-surface">
|
|
168
|
+
<MailHeader
|
|
169
|
+
title={title}
|
|
170
|
+
unreadCount={unreadCount}
|
|
171
|
+
// The list pane is narrow even on desktop, and the reading-pane
|
|
172
|
+
// toolbar owns the wide search; keep the header's search compact (a
|
|
173
|
+
// magnifier) at every width. On phone the magnifier opens the
|
|
174
|
+
// full-screen takeover above; on tablet it expands over the title.
|
|
175
|
+
isDesktop={false}
|
|
176
|
+
onMenuClick={() => layout?.openNav()}
|
|
177
|
+
searchValue={searchInput}
|
|
178
|
+
onSearchChange={onSearchChange}
|
|
179
|
+
onSearchClear={onSearchClear}
|
|
180
|
+
searchOpen={searchOpen}
|
|
181
|
+
onSearchOpenChange={setSearchOpen}
|
|
182
|
+
/>
|
|
183
|
+
<div className="min-h-0 flex-1">{body}</div>
|
|
184
|
+
{footer}
|
|
185
|
+
</section>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MailNav — the nav slot for AppShellSlotted.
|
|
3
|
+
*
|
|
4
|
+
* Renders a single "Mailboxes" navigation landmark. The kit shows this
|
|
5
|
+
* node either as a persistent pane (≥1024px) or inside its own slide-over
|
|
6
|
+
* Dialog (narrow) — never both — so there is exactly one nav landmark at
|
|
7
|
+
* any width. Below 1024px the slide-over also carries a Settings +
|
|
8
|
+
* bug-report footer (#685), since the mobile layout has no message toolbar
|
|
9
|
+
* to host them.
|
|
10
|
+
*/
|
|
11
|
+
import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
|
|
12
|
+
import { useAppShellLayout } from "@remit/ui";
|
|
13
|
+
import { Link } from "@tanstack/react-router";
|
|
14
|
+
import { LogOut, Settings } from "lucide-react";
|
|
15
|
+
import { AccountSession } from "@/auth/AccountSession";
|
|
16
|
+
import { MailSidebarAdapter } from "@/components/mail/MailSidebarAdapter";
|
|
17
|
+
import { BugReportButton } from "@/components/ui/BugReportButton";
|
|
18
|
+
|
|
19
|
+
interface MailNavProps {
|
|
20
|
+
accounts: RemitImapAccountResponse[];
|
|
21
|
+
onMailboxSelect?: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function MailNav({ accounts, onMailboxSelect }: MailNavProps) {
|
|
25
|
+
const layout = useAppShellLayout();
|
|
26
|
+
const isPane = layout?.showNavPane ?? true;
|
|
27
|
+
|
|
28
|
+
if (isPane) {
|
|
29
|
+
return <MailSidebarAdapter accounts={accounts} />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex h-full flex-col">
|
|
34
|
+
<div className="flex-1 overflow-y-auto">
|
|
35
|
+
<MailSidebarAdapter
|
|
36
|
+
accounts={accounts}
|
|
37
|
+
onMailboxSelect={onMailboxSelect}
|
|
38
|
+
variant="drawer"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
<div className="border-t border-line px-2 py-2">
|
|
42
|
+
<Link
|
|
43
|
+
to="/settings/accounts"
|
|
44
|
+
onClick={onMailboxSelect}
|
|
45
|
+
className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-sm text-fg-muted transition-colors hover:bg-surface hover:text-fg"
|
|
46
|
+
>
|
|
47
|
+
<Settings className="size-4 shrink-0" />
|
|
48
|
+
<span className="flex-1 truncate text-left">Settings</span>
|
|
49
|
+
</Link>
|
|
50
|
+
<BugReportButton variant="drawer" />
|
|
51
|
+
<AccountSession>
|
|
52
|
+
{({ signOut }) => (
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={() => {
|
|
56
|
+
onMailboxSelect?.();
|
|
57
|
+
signOut();
|
|
58
|
+
}}
|
|
59
|
+
className="flex min-h-11 w-full items-center gap-2 rounded-md px-2 py-1 text-sm text-fg-muted transition-colors hover:bg-surface hover:text-fg"
|
|
60
|
+
data-testid="sign-out-button"
|
|
61
|
+
>
|
|
62
|
+
<LogOut className="size-4 shrink-0" />
|
|
63
|
+
<span className="flex-1 truncate text-left">Sign out</span>
|
|
64
|
+
</button>
|
|
65
|
+
)}
|
|
66
|
+
</AccountSession>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mailboxOperationsListMailboxesOptions,
|
|
3
|
+
outboxOperationsListOutboxMessagesOptions,
|
|
4
|
+
threadOperationsListThreadsQueryKey,
|
|
5
|
+
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
6
|
+
import type {
|
|
7
|
+
RemitImapAccountResponse,
|
|
8
|
+
RemitImapMailboxResponse,
|
|
9
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
10
|
+
import type {
|
|
11
|
+
NavAccount,
|
|
12
|
+
NavLinkComponent,
|
|
13
|
+
NavMailboxRole,
|
|
14
|
+
} from "@remit/ui";
|
|
15
|
+
import { NavSidebar } from "@remit/ui";
|
|
16
|
+
import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
17
|
+
import {
|
|
18
|
+
Link,
|
|
19
|
+
useLocation,
|
|
20
|
+
useNavigate,
|
|
21
|
+
useParams,
|
|
22
|
+
} from "@tanstack/react-router";
|
|
23
|
+
import { useCallback, useMemo, useState } from "react";
|
|
24
|
+
import { useTranslation } from "react-i18next";
|
|
25
|
+
import {
|
|
26
|
+
buildMailboxRoleMap,
|
|
27
|
+
labelForMailbox,
|
|
28
|
+
shouldShowUnreadBadgeForRole,
|
|
29
|
+
} from "@/lib/folder-roles";
|
|
30
|
+
import { useMailContext } from "@/lib/mail-context";
|
|
31
|
+
import { isOutboxListRow } from "@/lib/outbox-status";
|
|
32
|
+
import {
|
|
33
|
+
loadSavedSearches,
|
|
34
|
+
removeSavedSearch,
|
|
35
|
+
saveSearch,
|
|
36
|
+
} from "@/lib/saved-searches";
|
|
37
|
+
|
|
38
|
+
interface MailSidebarAdapterProps {
|
|
39
|
+
accounts: RemitImapAccountResponse[];
|
|
40
|
+
/**
|
|
41
|
+
* Fires after the user selects a mailbox entry. The mobile drawer wires this
|
|
42
|
+
* to close itself on selection (#199). Desktop callers omit it.
|
|
43
|
+
*/
|
|
44
|
+
onMailboxSelect?: () => void;
|
|
45
|
+
/**
|
|
46
|
+
* "desktop" (default) wraps the nav in a full-height aside — Pane 1 of the
|
|
47
|
+
* 4-pane shell. "drawer" renders the nav body only so the mobile Drawer
|
|
48
|
+
* panel can pin a settings footer below it (#685).
|
|
49
|
+
*/
|
|
50
|
+
variant?: "desktop" | "drawer";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Translator = (key: string, fallback: string) => string;
|
|
54
|
+
|
|
55
|
+
const startsWithDigit = (str: string): boolean => /^\d/.test(str);
|
|
56
|
+
|
|
57
|
+
const compareLabelNames = (a: string, b: string): number => {
|
|
58
|
+
const aStartsWithDigit = startsWithDigit(a);
|
|
59
|
+
const bStartsWithDigit = startsWithDigit(b);
|
|
60
|
+
if (aStartsWithDigit && !bStartsWithDigit) return -1;
|
|
61
|
+
if (!aStartsWithDigit && bStartsWithDigit) return 1;
|
|
62
|
+
return a.localeCompare(b, undefined, { sensitivity: "base", numeric: true });
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Split a mailbox list into pinned system rows and alpha-sorted plain
|
|
67
|
+
* folders. RFC 032 exclusive-folder-appointment (#976): "system" now means
|
|
68
|
+
* "appointed to a role in this account's folderAppointments map" — nothing is
|
|
69
|
+
* deduped here, because two folders can never share a role by construction.
|
|
70
|
+
* The kit re-pins/orders system rows by role itself; the adapter only needs
|
|
71
|
+
* to alpha-order the folders.
|
|
72
|
+
*/
|
|
73
|
+
function sortMailboxes(
|
|
74
|
+
mailboxes: RemitImapMailboxResponse[],
|
|
75
|
+
roleMap: Map<string, NavMailboxRole>,
|
|
76
|
+
t: Translator,
|
|
77
|
+
): {
|
|
78
|
+
system: RemitImapMailboxResponse[];
|
|
79
|
+
labels: RemitImapMailboxResponse[];
|
|
80
|
+
} {
|
|
81
|
+
const system: RemitImapMailboxResponse[] = [];
|
|
82
|
+
const labels: RemitImapMailboxResponse[] = [];
|
|
83
|
+
|
|
84
|
+
for (const mailbox of mailboxes) {
|
|
85
|
+
if (roleMap.has(mailbox.mailboxId)) {
|
|
86
|
+
system.push(mailbox);
|
|
87
|
+
} else {
|
|
88
|
+
labels.push(mailbox);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
labels.sort((a, b) =>
|
|
93
|
+
compareLabelNames(
|
|
94
|
+
labelForMailbox(a, undefined, t),
|
|
95
|
+
labelForMailbox(b, undefined, t),
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return { system, labels };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Convert an API mailbox to a NavMailbox for the kit: `role` comes from the
|
|
104
|
+
* account's folder-role map (RFC 032 exclusive-folder-appointment, #976), the
|
|
105
|
+
* label follows `displayNameOverride` then the role's canonical name then the
|
|
106
|
+
* provider leaf, and the badge follows the role.
|
|
107
|
+
*/
|
|
108
|
+
function toNavMailbox(
|
|
109
|
+
mb: RemitImapMailboxResponse,
|
|
110
|
+
role: NavMailboxRole | undefined,
|
|
111
|
+
t: Translator,
|
|
112
|
+
) {
|
|
113
|
+
const showBadge = shouldShowUnreadBadgeForRole(role) && mb.unseenCount > 0;
|
|
114
|
+
return {
|
|
115
|
+
id: mb.mailboxId,
|
|
116
|
+
name: labelForMailbox(mb, role, t),
|
|
117
|
+
unseen: showBadge ? mb.unseenCount : undefined,
|
|
118
|
+
role,
|
|
119
|
+
fullPath: mb.fullPath,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolves the currently selected nav ID from the active route so the kit
|
|
125
|
+
* component can highlight the right item.
|
|
126
|
+
* - /mail/outbox → "outbox"
|
|
127
|
+
* - /mail/flagged → "flagged"
|
|
128
|
+
* - /mail/$mailboxId → mailboxId
|
|
129
|
+
* - /mail (daily brief) → "brief"
|
|
130
|
+
*/
|
|
131
|
+
function useSelectedNavId(): string {
|
|
132
|
+
const location = useLocation();
|
|
133
|
+
const params = useParams({ strict: false }) as { mailboxId?: string };
|
|
134
|
+
|
|
135
|
+
if (location.pathname.startsWith("/mail/outbox")) return "outbox";
|
|
136
|
+
if (location.pathname.startsWith("/mail/flagged")) return "flagged";
|
|
137
|
+
if (params.mailboxId) return params.mailboxId;
|
|
138
|
+
if (location.pathname === "/mail" || location.pathname === "/mail/")
|
|
139
|
+
return "brief";
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Pane 1 of the 4-pane shell. Data bridge that fetches mailboxes per account,
|
|
145
|
+
* maps them onto the kit's NavAccount shape, and renders each entry as a real
|
|
146
|
+
* router anchor via the kit's `linkComponent` render-prop. Replaces the retired
|
|
147
|
+
* local MailSidebar (#898).
|
|
148
|
+
*/
|
|
149
|
+
export function MailSidebarAdapter({
|
|
150
|
+
accounts,
|
|
151
|
+
onMailboxSelect,
|
|
152
|
+
variant = "desktop",
|
|
153
|
+
}: MailSidebarAdapterProps) {
|
|
154
|
+
const queryClient = useQueryClient();
|
|
155
|
+
const navigate = useNavigate();
|
|
156
|
+
const selectedNavId = useSelectedNavId();
|
|
157
|
+
const { searchInput, onSearchChange } = useMailContext();
|
|
158
|
+
const [savedSearches, setSavedSearches] = useState(loadSavedSearches);
|
|
159
|
+
const { t } = useTranslation("mail", { useSuspense: false });
|
|
160
|
+
const translator: Translator = useCallback(
|
|
161
|
+
(key, fallback) => t(key, { defaultValue: fallback }),
|
|
162
|
+
[t],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const mailboxQueries = useQueries({
|
|
166
|
+
queries: accounts.map((account) => ({
|
|
167
|
+
...mailboxOperationsListMailboxesOptions({
|
|
168
|
+
path: { accountId: account.accountId },
|
|
169
|
+
}),
|
|
170
|
+
// Mailboxes change rarely; cache forever, rely on explicit invalidation.
|
|
171
|
+
staleTime: Infinity,
|
|
172
|
+
})),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const { data: outboxResponse } = useQuery(
|
|
176
|
+
outboxOperationsListOutboxMessagesOptions(),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const outboxPendingCount = useMemo(
|
|
180
|
+
() =>
|
|
181
|
+
(outboxResponse?.items ?? []).filter((item) =>
|
|
182
|
+
isOutboxListRow(item.status),
|
|
183
|
+
).length,
|
|
184
|
+
[outboxResponse?.items],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const navAccounts: NavAccount[] = useMemo(() => {
|
|
188
|
+
return accounts.map((account, i) => {
|
|
189
|
+
const query = mailboxQueries[i];
|
|
190
|
+
const mailboxes = query?.data?.items ?? [];
|
|
191
|
+
const roleMap = buildMailboxRoleMap(account.folderAppointments);
|
|
192
|
+
const { system, labels } = sortMailboxes(mailboxes, roleMap, translator);
|
|
193
|
+
|
|
194
|
+
const status: NavAccount["status"] = query?.isError
|
|
195
|
+
? "error"
|
|
196
|
+
: query?.isLoading
|
|
197
|
+
? "loading"
|
|
198
|
+
: "ready";
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
id: account.accountId,
|
|
202
|
+
label: account.email,
|
|
203
|
+
email: account.email,
|
|
204
|
+
muted: Boolean(account.muted),
|
|
205
|
+
// Outbox is a shared singleton shown inside each account's section,
|
|
206
|
+
// matching existing sidebar behaviour.
|
|
207
|
+
outboxPending: outboxPendingCount,
|
|
208
|
+
status,
|
|
209
|
+
onRetry: () => query?.refetch(),
|
|
210
|
+
mailboxes: [...system, ...labels].map((mb) =>
|
|
211
|
+
toNavMailbox(mb, roleMap.get(mb.mailboxId), translator),
|
|
212
|
+
),
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}, [accounts, mailboxQueries, outboxPendingCount, translator]);
|
|
216
|
+
|
|
217
|
+
// Invalidate a mailbox's thread list on navigation so a re-visit always
|
|
218
|
+
// fetches fresh data (mirrors the retired MailboxItem behaviour).
|
|
219
|
+
const invalidateMailboxThreads = (mailboxId: string) => {
|
|
220
|
+
queryClient.invalidateQueries({
|
|
221
|
+
queryKey: threadOperationsListThreadsQueryKey({
|
|
222
|
+
path: { mailboxId },
|
|
223
|
+
query: { order: "desc" },
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const linkComponent: NavLinkComponent = ({
|
|
229
|
+
navId,
|
|
230
|
+
className,
|
|
231
|
+
ariaLabel,
|
|
232
|
+
title,
|
|
233
|
+
children,
|
|
234
|
+
onClick,
|
|
235
|
+
}) => {
|
|
236
|
+
if (navId === "brief") {
|
|
237
|
+
return (
|
|
238
|
+
<Link
|
|
239
|
+
to="/mail"
|
|
240
|
+
search={{ q: undefined, selectedMessageId: undefined }}
|
|
241
|
+
onClick={() => onClick?.()}
|
|
242
|
+
className={className}
|
|
243
|
+
aria-label={ariaLabel}
|
|
244
|
+
title={title}
|
|
245
|
+
>
|
|
246
|
+
{children}
|
|
247
|
+
</Link>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (navId === "outbox") {
|
|
251
|
+
return (
|
|
252
|
+
<Link
|
|
253
|
+
to="/mail/outbox"
|
|
254
|
+
search={{ q: undefined, selectedOutboxMessageId: undefined }}
|
|
255
|
+
onClick={() => onClick?.()}
|
|
256
|
+
className={className}
|
|
257
|
+
aria-label={ariaLabel}
|
|
258
|
+
title={title}
|
|
259
|
+
>
|
|
260
|
+
{children}
|
|
261
|
+
</Link>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (navId === "flagged") {
|
|
265
|
+
return (
|
|
266
|
+
<Link
|
|
267
|
+
to="/mail/flagged"
|
|
268
|
+
search={{ q: undefined, selectedMessageId: undefined }}
|
|
269
|
+
onClick={() => onClick?.()}
|
|
270
|
+
className={className}
|
|
271
|
+
aria-label={ariaLabel}
|
|
272
|
+
title={title}
|
|
273
|
+
>
|
|
274
|
+
{children}
|
|
275
|
+
</Link>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return (
|
|
279
|
+
<Link
|
|
280
|
+
to="/mail/$mailboxId"
|
|
281
|
+
params={{ mailboxId: navId }}
|
|
282
|
+
// Drop any stale search query / selected message when switching mailbox.
|
|
283
|
+
search={{ q: undefined, selectedMessageId: undefined }}
|
|
284
|
+
onClick={() => {
|
|
285
|
+
invalidateMailboxThreads(navId);
|
|
286
|
+
onClick?.();
|
|
287
|
+
}}
|
|
288
|
+
className={className}
|
|
289
|
+
aria-label={ariaLabel}
|
|
290
|
+
title={title}
|
|
291
|
+
>
|
|
292
|
+
{children}
|
|
293
|
+
</Link>
|
|
294
|
+
);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const handleSelectNav = () => {
|
|
298
|
+
// Navigation is handled by the router <Link>; this only runs the
|
|
299
|
+
// post-selection side effect (drawer auto-close on mobile).
|
|
300
|
+
onMailboxSelect?.();
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Saved searches (#428 follow-up, local-only MVP — see
|
|
304
|
+
// doc/design/flows/06-search.md). A saved query is the raw typed text; the
|
|
305
|
+
// existing token parser re-derives its chips on reselect, so there's nothing
|
|
306
|
+
// else to persist.
|
|
307
|
+
const trimmedSearchInput = searchInput.trim();
|
|
308
|
+
const saveableQuery =
|
|
309
|
+
trimmedSearchInput.length > 0 && !savedSearches.includes(trimmedSearchInput)
|
|
310
|
+
? trimmedSearchInput
|
|
311
|
+
: undefined;
|
|
312
|
+
|
|
313
|
+
const handleSaveCurrentSearch = useCallback(() => {
|
|
314
|
+
if (!saveableQuery) return;
|
|
315
|
+
setSavedSearches(saveSearch(saveableQuery));
|
|
316
|
+
}, [saveableQuery]);
|
|
317
|
+
|
|
318
|
+
const handleRemoveSavedSearch = useCallback((query: string) => {
|
|
319
|
+
setSavedSearches(removeSavedSearch(query));
|
|
320
|
+
}, []);
|
|
321
|
+
|
|
322
|
+
// Running a saved search re-uses the daily brief as the search surface (the
|
|
323
|
+
// cross-account default view) — the same destination the search field
|
|
324
|
+
// itself lands results in once a query is active.
|
|
325
|
+
const handleSelectSavedSearch = useCallback(
|
|
326
|
+
(query: string) => {
|
|
327
|
+
onSearchChange(query);
|
|
328
|
+
navigate({
|
|
329
|
+
to: "/mail",
|
|
330
|
+
search: { q: query, selectedMessageId: undefined },
|
|
331
|
+
});
|
|
332
|
+
onMailboxSelect?.();
|
|
333
|
+
},
|
|
334
|
+
[onSearchChange, navigate, onMailboxSelect],
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<NavSidebar
|
|
339
|
+
accounts={navAccounts}
|
|
340
|
+
selectedNavId={selectedNavId}
|
|
341
|
+
onSelectNav={handleSelectNav}
|
|
342
|
+
linkComponent={linkComponent}
|
|
343
|
+
variant={variant}
|
|
344
|
+
savedSearches={savedSearches}
|
|
345
|
+
saveableQuery={saveableQuery}
|
|
346
|
+
onSaveCurrentSearch={handleSaveCurrentSearch}
|
|
347
|
+
onSelectSavedSearch={handleSelectSavedSearch}
|
|
348
|
+
onRemoveSavedSearch={handleRemoveSavedSearch}
|
|
349
|
+
/>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MailViewChrome — the shared list-pane chrome for the inbox and flagged views.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the header-only `MailListHeader` and slots the `FilterSheet` expando
|
|
5
|
+
* directly into its body, exactly as the kit story does. The caller supplies the
|
|
6
|
+
* filter preset (`inboxFilterConfig` / `flaggedFilterConfig`) and owns the
|
|
7
|
+
* category / attribute / source selection state. The same filter config feeds
|
|
8
|
+
* the phone search takeover (via `MailListHeader`), so filters carry across.
|
|
9
|
+
*
|
|
10
|
+
* The daily brief no longer uses this: it composes `MailListHeader` with the kit
|
|
11
|
+
* `BriefSections`, which owns its own filter row (so there is exactly one filter
|
|
12
|
+
* surface and the section headers flatten correctly when filtered).
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
type FilterPreset,
|
|
16
|
+
FilterSheet,
|
|
17
|
+
type FilterSheetProps,
|
|
18
|
+
type SearchResult,
|
|
19
|
+
} from "@remit/ui";
|
|
20
|
+
import { type ReactNode, useState } from "react";
|
|
21
|
+
import { MailListHeader } from "./MailListHeader";
|
|
22
|
+
|
|
23
|
+
interface MailViewChromeProps {
|
|
24
|
+
title: string;
|
|
25
|
+
unreadCount: number;
|
|
26
|
+
preset: FilterPreset;
|
|
27
|
+
selectedCategory: string;
|
|
28
|
+
activeFilters: ReadonlySet<string>;
|
|
29
|
+
onSelectCategory: (id: string) => void;
|
|
30
|
+
onToggleFilter: (id: string) => void;
|
|
31
|
+
onSelectSource?: (id: string) => void;
|
|
32
|
+
onClearFilters: () => void;
|
|
33
|
+
/** The list body (sections / virtualized rows) rendered inside the expando. */
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
/** Pinned below the scrollable list (e.g. the keyboard hint bar). */
|
|
36
|
+
footer?: ReactNode;
|
|
37
|
+
/** Literal/instant results — the "Top matches" section. */
|
|
38
|
+
searchResults?: SearchResult[];
|
|
39
|
+
searchLoading?: boolean;
|
|
40
|
+
/** Semantic results — the "Related" section. */
|
|
41
|
+
relatedResults?: SearchResult[];
|
|
42
|
+
relatedLoading?: boolean;
|
|
43
|
+
onSelectSearchResult?: (result: SearchResult) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function MailViewChrome({
|
|
47
|
+
title,
|
|
48
|
+
unreadCount,
|
|
49
|
+
preset,
|
|
50
|
+
selectedCategory,
|
|
51
|
+
activeFilters,
|
|
52
|
+
onSelectCategory,
|
|
53
|
+
onToggleFilter,
|
|
54
|
+
onSelectSource,
|
|
55
|
+
onClearFilters,
|
|
56
|
+
children,
|
|
57
|
+
footer,
|
|
58
|
+
searchResults,
|
|
59
|
+
searchLoading,
|
|
60
|
+
relatedResults,
|
|
61
|
+
relatedLoading,
|
|
62
|
+
onSelectSearchResult,
|
|
63
|
+
}: MailViewChromeProps) {
|
|
64
|
+
const [expanded, setExpanded] = useState(false);
|
|
65
|
+
|
|
66
|
+
const filterConfig: Omit<FilterSheetProps, "children"> = {
|
|
67
|
+
categories: preset.categories,
|
|
68
|
+
filters: preset.filters,
|
|
69
|
+
sources: preset.sources,
|
|
70
|
+
selectedCategory,
|
|
71
|
+
activeFilters,
|
|
72
|
+
expanded,
|
|
73
|
+
onExpandedChange: setExpanded,
|
|
74
|
+
onSelectCategory,
|
|
75
|
+
onSelectSource,
|
|
76
|
+
onToggleFilter,
|
|
77
|
+
onClear: onClearFilters,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<MailListHeader
|
|
82
|
+
title={title}
|
|
83
|
+
unreadCount={unreadCount}
|
|
84
|
+
footer={footer}
|
|
85
|
+
searchFilter={filterConfig}
|
|
86
|
+
searchResults={searchResults}
|
|
87
|
+
searchLoading={searchLoading}
|
|
88
|
+
relatedResults={relatedResults}
|
|
89
|
+
relatedLoading={relatedLoading}
|
|
90
|
+
onSelectSearchResult={onSelectSearchResult}
|
|
91
|
+
>
|
|
92
|
+
<FilterSheet {...filterConfig}>{children}</FilterSheet>
|
|
93
|
+
</MailListHeader>
|
|
94
|
+
);
|
|
95
|
+
}
|