@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,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DailyBrief — unified cross-account message digest.
|
|
3
|
+
*
|
|
4
|
+
* Renders one section per message category (Personal / Transactional /
|
|
5
|
+
* Newsletter / Marketing / Social / Automated) from the GET /threads endpoint.
|
|
6
|
+
* Starred mail is not a section — Flagged is a virtual mailbox in the nav. The
|
|
7
|
+
* brief defaults to the cross-account aggregate; the kit `BriefSections` owns
|
|
8
|
+
* the filter row (categories + attribute chips + the account source group) and
|
|
9
|
+
* the flatten-when-filtered behavior, while `MailListHeader` provides the title,
|
|
10
|
+
* unread count, and search. Account switching also lives in the nav sidebar —
|
|
11
|
+
* the account source group only appears when more than one account feeds the
|
|
12
|
+
* brief.
|
|
13
|
+
*
|
|
14
|
+
* Loading: skeleton rows on first paint, patch-in-place on refetch.
|
|
15
|
+
* Error: per-section; the brief still renders other sections.
|
|
16
|
+
* Empty: "You're caught up" message.
|
|
17
|
+
*/
|
|
18
|
+
import {
|
|
19
|
+
mailboxOperationsListMailboxesOptions,
|
|
20
|
+
unifiedThreadOperationsListAllThreadsOptions,
|
|
21
|
+
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
22
|
+
import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
|
|
23
|
+
import {
|
|
24
|
+
Avatar,
|
|
25
|
+
type BriefCategoryFilter,
|
|
26
|
+
BriefSections,
|
|
27
|
+
briefFilterConfig,
|
|
28
|
+
ComfortableRowTextContent,
|
|
29
|
+
cn,
|
|
30
|
+
comfortableRowClass,
|
|
31
|
+
type FilterSheetProps,
|
|
32
|
+
type FilterSheetSource,
|
|
33
|
+
KeyboardHintBar,
|
|
34
|
+
type SearchResult,
|
|
35
|
+
type ThreadRowData,
|
|
36
|
+
type ThreadSection,
|
|
37
|
+
} from "@remit/ui";
|
|
38
|
+
import { useQueries, useQuery } from "@tanstack/react-query";
|
|
39
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
40
|
+
import { AlertCircle, RefreshCw, Sparkles } from "lucide-react";
|
|
41
|
+
import { useCallback, useMemo, useState } from "react";
|
|
42
|
+
import { useIsDesktop } from "@/hooks/useMediaQuery";
|
|
43
|
+
import { useSemanticSearch } from "@/hooks/useSemanticSearch";
|
|
44
|
+
import { sortAccountsByCreatedAt } from "@/lib/account-order";
|
|
45
|
+
import {
|
|
46
|
+
groupBriefSections,
|
|
47
|
+
matchesBriefSearch,
|
|
48
|
+
matchesSearchTokens,
|
|
49
|
+
toThreadRowData,
|
|
50
|
+
} from "@/lib/brief";
|
|
51
|
+
import { isServerError } from "@/lib/error-classifier";
|
|
52
|
+
import { useMailContext } from "@/lib/mail-context";
|
|
53
|
+
import { relatedSearchResults, rowToSearchResult } from "@/lib/search-result";
|
|
54
|
+
import { parseSearchTokens } from "@/lib/search-tokens";
|
|
55
|
+
import { MailListHeader } from "./MailListHeader";
|
|
56
|
+
|
|
57
|
+
/* The brief's attribute chips as predicates (mirrors the kit `briefFilterChips`
|
|
58
|
+
ids) so the phone search takeover narrows results the same way the list does. */
|
|
59
|
+
const BRIEF_SEARCH_PREDICATES: Record<string, (t: ThreadRowData) => boolean> = {
|
|
60
|
+
unread: (t) => !t.isRead,
|
|
61
|
+
attachment: (t) => t.hasAttachment === true,
|
|
62
|
+
contacts: (t) => t.trust === "vip" || t.trust === "wellknown",
|
|
63
|
+
today: (t) =>
|
|
64
|
+
t.sentDate != null &&
|
|
65
|
+
new Date(t.sentDate).toDateString() === new Date().toDateString(),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Skeleton
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
const SectionSkeleton = () => (
|
|
73
|
+
<div className="animate-pulse">
|
|
74
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
75
|
+
<div
|
|
76
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: skeleton rows have no stable id
|
|
77
|
+
key={i}
|
|
78
|
+
className="flex items-start gap-3 py-2 pl-5 pr-4 border-b border-line"
|
|
79
|
+
>
|
|
80
|
+
<div className="size-7 rounded-full bg-surface-sunken shrink-0 mt-0.5" />
|
|
81
|
+
<div className="flex-1 space-y-1.5">
|
|
82
|
+
<div className="flex justify-between gap-2">
|
|
83
|
+
<div className="h-3.5 bg-surface-sunken rounded w-28" />
|
|
84
|
+
<div className="h-3 bg-surface-sunken rounded w-12" />
|
|
85
|
+
</div>
|
|
86
|
+
<div className="h-3.5 bg-surface-sunken rounded w-48" />
|
|
87
|
+
<div className="h-3 bg-surface-sunken rounded w-full" />
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Error banner (per-account connection failure)
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
interface ErrorBannerProps {
|
|
99
|
+
accountEmail: string;
|
|
100
|
+
accountId: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const ErrorBanner = ({ accountEmail }: ErrorBannerProps) => {
|
|
104
|
+
const navigate = useNavigate();
|
|
105
|
+
return (
|
|
106
|
+
<div className="flex items-center gap-2 px-row-inset py-2 border-b border-line bg-danger-soft/40 text-xs text-danger">
|
|
107
|
+
<AlertCircle className="size-3.5 shrink-0" />
|
|
108
|
+
<span className="flex-1 truncate">{accountEmail} can't connect</span>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={() => navigate({ to: "/settings/accounts" })}
|
|
112
|
+
className="shrink-0 underline text-danger hover:opacity-80"
|
|
113
|
+
>
|
|
114
|
+
Reconnect
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Brief row — a navigation-aware row satisfying remit-ui's BriefRowComponent
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
const BriefRow = ({
|
|
125
|
+
thread,
|
|
126
|
+
active,
|
|
127
|
+
onClick,
|
|
128
|
+
}: {
|
|
129
|
+
thread: ThreadRowData;
|
|
130
|
+
active?: boolean;
|
|
131
|
+
onClick?: () => void;
|
|
132
|
+
}) => {
|
|
133
|
+
const unread = !thread.isRead;
|
|
134
|
+
return (
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={onClick}
|
|
138
|
+
className={cn("group w-full", comfortableRowClass({ active }))}
|
|
139
|
+
>
|
|
140
|
+
{unread && (
|
|
141
|
+
<span className="absolute left-1.5 top-1/2 size-1.5 -translate-y-1/2 rounded-full bg-accent" />
|
|
142
|
+
)}
|
|
143
|
+
<Avatar name={thread.fromName} email={thread.fromEmail} size="sm" />
|
|
144
|
+
<ComfortableRowTextContent thread={thread} />
|
|
145
|
+
</button>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Main component
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
interface DailyBriefProps {
|
|
154
|
+
accounts: RemitImapAccountResponse[];
|
|
155
|
+
selectedMessageId?: string;
|
|
156
|
+
/** Opens an in-list brief row (resolved by messageId against the loaded list). */
|
|
157
|
+
onSelectMessage?: (id: string) => void;
|
|
158
|
+
/**
|
|
159
|
+
* Opens a search result. A semantic "Related" hit carries its thread + mailbox
|
|
160
|
+
* so it opens even when its message isn't in the loaded brief list.
|
|
161
|
+
*/
|
|
162
|
+
onSelectSearchResult?: (result: SearchResult) => void;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function DailyBrief({
|
|
166
|
+
accounts,
|
|
167
|
+
selectedMessageId,
|
|
168
|
+
onSelectMessage,
|
|
169
|
+
onSelectSearchResult,
|
|
170
|
+
}: DailyBriefProps) {
|
|
171
|
+
const { searchQuery, mailboxNameIndex, accountNameIndex } = useMailContext();
|
|
172
|
+
const isDesktop = useIsDesktop();
|
|
173
|
+
|
|
174
|
+
const nonMuted = useMemo(
|
|
175
|
+
() => sortAccountsByCreatedAt(accounts.filter((a) => !a.muted?.value)),
|
|
176
|
+
[accounts],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// "all" = the cross-account aggregate (the brief's default). Account
|
|
180
|
+
// switching also lives in the nav sidebar; this source group is a convenience
|
|
181
|
+
// shown only when more than one account feeds the brief. The category axis and
|
|
182
|
+
// attribute chips are owned by the kit `BriefSections` filter row; the brief
|
|
183
|
+
// only controls the category (so it can drive the flatten-when-filtered path)
|
|
184
|
+
// and the account source.
|
|
185
|
+
const [selectedAccountId, setSelectedAccountId] = useState("all");
|
|
186
|
+
const [selectedCategory, setSelectedCategory] =
|
|
187
|
+
useState<BriefCategoryFilter>("all");
|
|
188
|
+
|
|
189
|
+
// Attribute chips for the phone search takeover. The brief list's own chips
|
|
190
|
+
// live inside the kit `BriefSections`; the takeover is a separate surface, so
|
|
191
|
+
// it carries its own additive set (category + account are shared above).
|
|
192
|
+
const [searchAttributes, setSearchAttributes] = useState<ReadonlySet<string>>(
|
|
193
|
+
new Set(),
|
|
194
|
+
);
|
|
195
|
+
const [searchExpanded, setSearchExpanded] = useState(false);
|
|
196
|
+
const toggleSearchAttribute = useCallback((id: string) => {
|
|
197
|
+
setSearchAttributes((prev) => {
|
|
198
|
+
const next = new Set(prev);
|
|
199
|
+
if (next.has(id)) next.delete(id);
|
|
200
|
+
else next.add(id);
|
|
201
|
+
return next;
|
|
202
|
+
});
|
|
203
|
+
}, []);
|
|
204
|
+
|
|
205
|
+
// --- Unified threads query ---
|
|
206
|
+
const {
|
|
207
|
+
data: threadsData,
|
|
208
|
+
isLoading,
|
|
209
|
+
isError,
|
|
210
|
+
refetch,
|
|
211
|
+
} = useQuery({
|
|
212
|
+
...unifiedThreadOperationsListAllThreadsOptions(),
|
|
213
|
+
staleTime: 60_000,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// --- Per-account mailbox list for unread counts and error detection ---
|
|
217
|
+
const mailboxQueries = useQueries({
|
|
218
|
+
queries: nonMuted.map((account) => ({
|
|
219
|
+
...mailboxOperationsListMailboxesOptions({
|
|
220
|
+
path: { accountId: account.accountId },
|
|
221
|
+
}),
|
|
222
|
+
staleTime: Infinity,
|
|
223
|
+
// A 4xx here is the account's own problem (IMAP down, auth expired) and
|
|
224
|
+
// renders the inline "Reconnect" banner below — opt it out of the global
|
|
225
|
+
// fatal overlay. A 5xx is OUR API breaking, not the account, and still
|
|
226
|
+
// escalates globally (meta.softError is ignored for 5xx — #1059).
|
|
227
|
+
meta: { softError: true },
|
|
228
|
+
})),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Build accountId → unseen map for source counts
|
|
232
|
+
const unseenByAccount = useMemo<Map<string, number>>(() => {
|
|
233
|
+
const map = new Map<string, number>();
|
|
234
|
+
for (let i = 0; i < nonMuted.length; i++) {
|
|
235
|
+
const accountId = nonMuted[i].accountId;
|
|
236
|
+
const mailboxes = mailboxQueries[i]?.data?.items ?? [];
|
|
237
|
+
const total = mailboxes.reduce(
|
|
238
|
+
(sum, mb) => sum + (mb.unseenCount ?? 0),
|
|
239
|
+
0,
|
|
240
|
+
);
|
|
241
|
+
map.set(accountId, total);
|
|
242
|
+
}
|
|
243
|
+
return map;
|
|
244
|
+
}, [nonMuted, mailboxQueries]);
|
|
245
|
+
|
|
246
|
+
// Per-account connection failures: accounts whose mailbox list failed for a
|
|
247
|
+
// reason that is genuinely the account's (e.g. IMAP down, auth expired) — a
|
|
248
|
+
// 4xx. A first-party 5xx is OUR API breaking, not the account being
|
|
249
|
+
// unreachable, so it must NOT render the misleading "can't connect /
|
|
250
|
+
// Reconnect" banner; the global escalation overlay (QueryCache.onError)
|
|
251
|
+
// handles it instead.
|
|
252
|
+
const failedAccounts = useMemo<RemitImapAccountResponse[]>(() => {
|
|
253
|
+
return nonMuted.filter((_, i) => {
|
|
254
|
+
const query = mailboxQueries[i];
|
|
255
|
+
if (!query?.isError) return false;
|
|
256
|
+
return !isServerError(query.error);
|
|
257
|
+
});
|
|
258
|
+
}, [nonMuted, mailboxQueries]);
|
|
259
|
+
|
|
260
|
+
const { freeText: sq, tokens: queryTokens } = parseSearchTokens(
|
|
261
|
+
searchQuery.trim().toLowerCase(),
|
|
262
|
+
{ mailboxesByName: mailboxNameIndex, accountsByName: accountNameIndex },
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
// Convert API rows to ThreadRowData, narrowing only by the selected account
|
|
266
|
+
// and the free-text search plus any filter tokens (`from:`, `has:attachment`,
|
|
267
|
+
// `is:unread`, `before:`/`after:`, `in:`, `account:`) parsed out of the
|
|
268
|
+
// query. The category axis and the attribute chips are the kit
|
|
269
|
+
// `BriefSections` filter row's job, so the full per-category sections are
|
|
270
|
+
// handed to it; it groups, narrows, and flattens.
|
|
271
|
+
const filteredRows = useMemo<ThreadRowData[]>(() => {
|
|
272
|
+
const raw = threadsData?.items ?? [];
|
|
273
|
+
return raw
|
|
274
|
+
.filter(
|
|
275
|
+
(t) =>
|
|
276
|
+
selectedAccountId === "all" ||
|
|
277
|
+
(t.accountId ?? t.accountConfigId) === selectedAccountId,
|
|
278
|
+
)
|
|
279
|
+
.map(toThreadRowData)
|
|
280
|
+
.filter(
|
|
281
|
+
(t) =>
|
|
282
|
+
(!sq || matchesBriefSearch(t, sq)) &&
|
|
283
|
+
matchesSearchTokens(t, queryTokens),
|
|
284
|
+
);
|
|
285
|
+
}, [threadsData, selectedAccountId, sq, queryTokens]);
|
|
286
|
+
|
|
287
|
+
const sections = useMemo<ThreadSection[]>(
|
|
288
|
+
() => groupBriefSections(filteredRows),
|
|
289
|
+
[filteredRows],
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const accountSources = useMemo<FilterSheetSource[]>(() => {
|
|
293
|
+
if (nonMuted.length <= 1) return [];
|
|
294
|
+
return [
|
|
295
|
+
{ id: "all", label: "All", active: selectedAccountId === "all" },
|
|
296
|
+
...nonMuted.map((account) => ({
|
|
297
|
+
id: account.accountId,
|
|
298
|
+
label: account.email.split("@")[0] ?? account.email,
|
|
299
|
+
count: unseenByAccount.get(account.accountId),
|
|
300
|
+
active: selectedAccountId === account.accountId,
|
|
301
|
+
})),
|
|
302
|
+
];
|
|
303
|
+
}, [nonMuted, unseenByAccount, selectedAccountId]);
|
|
304
|
+
|
|
305
|
+
const mutedCount = useMemo(
|
|
306
|
+
() => accounts.filter((a) => a.muted?.value).length,
|
|
307
|
+
[accounts],
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const totalUnseen = useMemo(
|
|
311
|
+
() => Array.from(unseenByAccount.values()).reduce((a, b) => a + b, 0),
|
|
312
|
+
[unseenByAccount],
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// The phone search takeover renders the account/free-text-narrowed rows,
|
|
316
|
+
// further narrowed by the shared category and the takeover's attribute chips.
|
|
317
|
+
const searchResults = useMemo<SearchResult[]>(() => {
|
|
318
|
+
const predicates = Array.from(searchAttributes)
|
|
319
|
+
.map((id) => BRIEF_SEARCH_PREDICATES[id])
|
|
320
|
+
.filter((p): p is (t: ThreadRowData) => boolean => p != null);
|
|
321
|
+
return filteredRows
|
|
322
|
+
.filter(
|
|
323
|
+
(t) =>
|
|
324
|
+
(selectedCategory === "all" || t.category === selectedCategory) &&
|
|
325
|
+
predicates.every((p) => p(t)),
|
|
326
|
+
)
|
|
327
|
+
.map(rowToSearchResult);
|
|
328
|
+
}, [filteredRows, selectedCategory, searchAttributes]);
|
|
329
|
+
|
|
330
|
+
// "Related" (semantic) spans every account here — the brief is the
|
|
331
|
+
// cross-account view, so no mailbox scope. Dedupe against the literal "Top
|
|
332
|
+
// matches" by thread; the brief rows key on messageId, so resolve each back to
|
|
333
|
+
// its thread via the raw threads.
|
|
334
|
+
const { hits: semanticHits, isLoading: relatedLoading } = useSemanticSearch();
|
|
335
|
+
const relatedResults = useMemo<SearchResult[]>(() => {
|
|
336
|
+
const threadByMessageId = new Map<string, string>();
|
|
337
|
+
for (const thread of threadsData?.items ?? []) {
|
|
338
|
+
threadByMessageId.set(thread.messageId, thread.threadId);
|
|
339
|
+
}
|
|
340
|
+
const literalThreadIds = searchResults
|
|
341
|
+
.map((result) => threadByMessageId.get(result.id))
|
|
342
|
+
.filter((id): id is string => id != null);
|
|
343
|
+
return relatedSearchResults(semanticHits, literalThreadIds);
|
|
344
|
+
}, [semanticHits, searchResults, threadsData]);
|
|
345
|
+
|
|
346
|
+
const searchFilterConfig = useMemo<Omit<FilterSheetProps, "children">>(() => {
|
|
347
|
+
const preset = briefFilterConfig(
|
|
348
|
+
accountSources.map((s) => ({
|
|
349
|
+
id: s.id,
|
|
350
|
+
label: s.label,
|
|
351
|
+
count: s.count,
|
|
352
|
+
active: s.active,
|
|
353
|
+
})),
|
|
354
|
+
);
|
|
355
|
+
return {
|
|
356
|
+
categories: preset.categories,
|
|
357
|
+
filters: preset.filters,
|
|
358
|
+
sources: preset.sources,
|
|
359
|
+
sourcesNote: mutedCount > 0 ? `+${mutedCount} muted` : undefined,
|
|
360
|
+
selectedCategory,
|
|
361
|
+
activeFilters: searchAttributes,
|
|
362
|
+
expanded: searchExpanded,
|
|
363
|
+
onExpandedChange: setSearchExpanded,
|
|
364
|
+
onSelectCategory: (id: string) =>
|
|
365
|
+
setSelectedCategory(id as BriefCategoryFilter),
|
|
366
|
+
onSelectSource: setSelectedAccountId,
|
|
367
|
+
onToggleFilter: toggleSearchAttribute,
|
|
368
|
+
onClear: () => {
|
|
369
|
+
setSelectedCategory("all");
|
|
370
|
+
setSelectedAccountId("all");
|
|
371
|
+
setSearchAttributes(new Set());
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}, [
|
|
375
|
+
accountSources,
|
|
376
|
+
mutedCount,
|
|
377
|
+
selectedCategory,
|
|
378
|
+
searchAttributes,
|
|
379
|
+
searchExpanded,
|
|
380
|
+
toggleSearchAttribute,
|
|
381
|
+
]);
|
|
382
|
+
|
|
383
|
+
// The brief is genuinely empty (caught up) only when nothing is narrowing the
|
|
384
|
+
// view: no account source and no search. When a source/search yields nothing,
|
|
385
|
+
// the BriefSections filter row stays so the user can clear it.
|
|
386
|
+
const caughtUp =
|
|
387
|
+
sections.length === 0 && selectedAccountId === "all" && sq.length === 0;
|
|
388
|
+
|
|
389
|
+
const stateBody = isLoading ? (
|
|
390
|
+
<div className="h-full overflow-y-auto">
|
|
391
|
+
<SectionSkeleton />
|
|
392
|
+
<SectionSkeleton />
|
|
393
|
+
</div>
|
|
394
|
+
) : isError ? (
|
|
395
|
+
<div className="flex h-full flex-col items-center justify-center gap-3 py-12 text-sm text-fg-muted">
|
|
396
|
+
<AlertCircle className="size-8 text-danger" />
|
|
397
|
+
<p>Couldn't load your messages</p>
|
|
398
|
+
<button
|
|
399
|
+
type="button"
|
|
400
|
+
onClick={() => refetch()}
|
|
401
|
+
className="flex items-center gap-1 text-accent underline text-xs"
|
|
402
|
+
>
|
|
403
|
+
<RefreshCw className="size-3.5" />
|
|
404
|
+
Try again
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
407
|
+
) : caughtUp ? (
|
|
408
|
+
<div className="flex h-full flex-col items-center justify-center gap-2 py-12 text-center px-4">
|
|
409
|
+
<Sparkles className="size-8 text-fg-subtle" />
|
|
410
|
+
<p className="text-sm font-medium text-fg">You're caught up</p>
|
|
411
|
+
<p className="text-xs text-fg-subtle">Nothing needs attention.</p>
|
|
412
|
+
</div>
|
|
413
|
+
) : (
|
|
414
|
+
<BriefSections
|
|
415
|
+
sections={sections}
|
|
416
|
+
Row={BriefRow}
|
|
417
|
+
briefCategory={selectedCategory}
|
|
418
|
+
onSelectBriefCategory={setSelectedCategory}
|
|
419
|
+
sources={accountSources}
|
|
420
|
+
sourcesNote={mutedCount > 0 ? `+${mutedCount} muted` : undefined}
|
|
421
|
+
onSelectSource={setSelectedAccountId}
|
|
422
|
+
selectedThreadId={selectedMessageId}
|
|
423
|
+
onSelectThread={onSelectMessage}
|
|
424
|
+
/>
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<MailListHeader
|
|
429
|
+
title="Daily brief"
|
|
430
|
+
unreadCount={totalUnseen}
|
|
431
|
+
footer={isDesktop ? <KeyboardHintBar /> : undefined}
|
|
432
|
+
searchFilter={searchFilterConfig}
|
|
433
|
+
searchResults={searchResults}
|
|
434
|
+
searchLoading={isLoading}
|
|
435
|
+
relatedResults={relatedResults}
|
|
436
|
+
relatedLoading={relatedLoading}
|
|
437
|
+
onSelectSearchResult={onSelectSearchResult}
|
|
438
|
+
>
|
|
439
|
+
<div className="flex h-full flex-col">
|
|
440
|
+
{failedAccounts.map((account) => (
|
|
441
|
+
<ErrorBanner
|
|
442
|
+
key={account.accountId}
|
|
443
|
+
accountEmail={account.email}
|
|
444
|
+
accountId={account.accountId}
|
|
445
|
+
/>
|
|
446
|
+
))}
|
|
447
|
+
<div className="min-h-0 flex-1">{stateBody}</div>
|
|
448
|
+
</div>
|
|
449
|
+
</MailListHeader>
|
|
450
|
+
);
|
|
451
|
+
}
|