@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,218 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
parseSearchTokens,
|
|
5
|
+
removeSearchToken,
|
|
6
|
+
searchTokenLabel,
|
|
7
|
+
} from "./search-tokens.js";
|
|
8
|
+
|
|
9
|
+
describe("parseSearchTokens", () => {
|
|
10
|
+
it("returns the query untouched when there are no tokens", () => {
|
|
11
|
+
const result = parseSearchTokens("parcel delivery confirmation");
|
|
12
|
+
assert.equal(result.freeText, "parcel delivery confirmation");
|
|
13
|
+
assert.deepEqual(result.tokens, []);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("parses from:", () => {
|
|
17
|
+
const result = parseSearchTokens("invoice from:alice@example.com");
|
|
18
|
+
assert.equal(result.freeText, "invoice");
|
|
19
|
+
assert.deepEqual(result.tokens, [
|
|
20
|
+
{
|
|
21
|
+
type: "from",
|
|
22
|
+
raw: "from:alice@example.com",
|
|
23
|
+
value: "alice@example.com",
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("parses has:attachment", () => {
|
|
29
|
+
const result = parseSearchTokens("receipts has:attachment");
|
|
30
|
+
assert.equal(result.freeText, "receipts");
|
|
31
|
+
assert.deepEqual(result.tokens, [
|
|
32
|
+
{ type: "hasAttachment", raw: "has:attachment" },
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("parses is:unread case-insensitively", () => {
|
|
37
|
+
const result = parseSearchTokens("IS:UNREAD receipts");
|
|
38
|
+
assert.equal(result.freeText, "receipts");
|
|
39
|
+
assert.deepEqual(result.tokens, [{ type: "isUnread", raw: "IS:UNREAD" }]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("parses before: and after: as epoch seconds", () => {
|
|
43
|
+
const result = parseSearchTokens("after:2024-01-01 before:2024-02-01 tax");
|
|
44
|
+
assert.equal(result.freeText, "tax");
|
|
45
|
+
assert.deepEqual(result.tokens, [
|
|
46
|
+
{
|
|
47
|
+
type: "after",
|
|
48
|
+
raw: "after:2024-01-01",
|
|
49
|
+
value: "2024-01-01",
|
|
50
|
+
epochSeconds: Date.parse("2024-01-01T00:00:00Z") / 1000,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "before",
|
|
54
|
+
raw: "before:2024-02-01",
|
|
55
|
+
value: "2024-02-01",
|
|
56
|
+
epochSeconds: Date.parse("2024-02-01T00:00:00Z") / 1000,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("leaves a malformed date token in the free text", () => {
|
|
62
|
+
const result = parseSearchTokens("before:not-a-date tax");
|
|
63
|
+
assert.equal(result.freeText, "before:not-a-date tax");
|
|
64
|
+
assert.deepEqual(result.tokens, []);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("leaves in:/account: as free text with no name index", () => {
|
|
68
|
+
const result = parseSearchTokens("in:archive account:work invoice");
|
|
69
|
+
assert.equal(result.freeText, "in:archive account:work invoice");
|
|
70
|
+
assert.deepEqual(result.tokens, []);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("resolves in: against a supplied mailbox name index", () => {
|
|
74
|
+
const result = parseSearchTokens("in:archive invoice", {
|
|
75
|
+
mailboxesByName: new Map([["archive", "mailbox-1"]]),
|
|
76
|
+
});
|
|
77
|
+
assert.equal(result.freeText, "invoice");
|
|
78
|
+
assert.deepEqual(result.tokens, [
|
|
79
|
+
{
|
|
80
|
+
type: "in",
|
|
81
|
+
raw: "in:archive",
|
|
82
|
+
value: "archive",
|
|
83
|
+
mailboxId: "mailbox-1",
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("resolves in: case-insensitively", () => {
|
|
89
|
+
const result = parseSearchTokens("IN:Archive invoice", {
|
|
90
|
+
mailboxesByName: new Map([["archive", "mailbox-1"]]),
|
|
91
|
+
});
|
|
92
|
+
assert.deepEqual(result.tokens, [
|
|
93
|
+
{
|
|
94
|
+
type: "in",
|
|
95
|
+
raw: "IN:Archive",
|
|
96
|
+
value: "Archive",
|
|
97
|
+
mailboxId: "mailbox-1",
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("leaves in: as free text when the name isn't in the index", () => {
|
|
103
|
+
const result = parseSearchTokens("in:nonexistent invoice", {
|
|
104
|
+
mailboxesByName: new Map([["archive", "mailbox-1"]]),
|
|
105
|
+
});
|
|
106
|
+
assert.equal(result.freeText, "in:nonexistent invoice");
|
|
107
|
+
assert.deepEqual(result.tokens, []);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("resolves account: against a supplied account name index", () => {
|
|
111
|
+
const result = parseSearchTokens("account:work invoice", {
|
|
112
|
+
accountsByName: new Map([["work", "account-1"]]),
|
|
113
|
+
});
|
|
114
|
+
assert.equal(result.freeText, "invoice");
|
|
115
|
+
assert.deepEqual(result.tokens, [
|
|
116
|
+
{
|
|
117
|
+
type: "account",
|
|
118
|
+
raw: "account:work",
|
|
119
|
+
value: "work",
|
|
120
|
+
accountId: "account-1",
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("leaves account: as free text when the name isn't in the index", () => {
|
|
126
|
+
const result = parseSearchTokens("account:nonexistent invoice", {
|
|
127
|
+
accountsByName: new Map([["work", "account-1"]]),
|
|
128
|
+
});
|
|
129
|
+
assert.equal(result.freeText, "account:nonexistent invoice");
|
|
130
|
+
assert.deepEqual(result.tokens, []);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("parses multiple tokens alongside free text", () => {
|
|
134
|
+
const result = parseSearchTokens(
|
|
135
|
+
"parcel from:dhl.com has:attachment is:unread",
|
|
136
|
+
);
|
|
137
|
+
assert.equal(result.freeText, "parcel");
|
|
138
|
+
assert.equal(result.tokens.length, 3);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("ignores a bare 'from:' with no value", () => {
|
|
142
|
+
const result = parseSearchTokens("from:");
|
|
143
|
+
assert.equal(result.freeText, "from:");
|
|
144
|
+
assert.deepEqual(result.tokens, []);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("removeSearchToken", () => {
|
|
149
|
+
it("removes the token's raw text and collapses whitespace", () => {
|
|
150
|
+
const tokens = parseSearchTokens("parcel from:dhl.com delivery").tokens;
|
|
151
|
+
const next = removeSearchToken(
|
|
152
|
+
"parcel from:dhl.com delivery",
|
|
153
|
+
tokens[0] as NonNullable<(typeof tokens)[0]>,
|
|
154
|
+
);
|
|
155
|
+
assert.equal(next, "parcel delivery");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("is a no-op when the token isn't present", () => {
|
|
159
|
+
const next = removeSearchToken("parcel delivery", {
|
|
160
|
+
type: "hasAttachment",
|
|
161
|
+
raw: "has:attachment",
|
|
162
|
+
});
|
|
163
|
+
assert.equal(next, "parcel delivery");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("searchTokenLabel", () => {
|
|
168
|
+
it("labels each token type in plain words", () => {
|
|
169
|
+
assert.equal(
|
|
170
|
+
searchTokenLabel({ type: "from", raw: "from:alice", value: "alice" }),
|
|
171
|
+
"From: alice",
|
|
172
|
+
);
|
|
173
|
+
assert.equal(
|
|
174
|
+
searchTokenLabel({ type: "hasAttachment", raw: "has:attachment" }),
|
|
175
|
+
"Has attachment",
|
|
176
|
+
);
|
|
177
|
+
assert.equal(
|
|
178
|
+
searchTokenLabel({ type: "isUnread", raw: "is:unread" }),
|
|
179
|
+
"Unread",
|
|
180
|
+
);
|
|
181
|
+
assert.equal(
|
|
182
|
+
searchTokenLabel({
|
|
183
|
+
type: "before",
|
|
184
|
+
raw: "before:2024-01-01",
|
|
185
|
+
value: "2024-01-01",
|
|
186
|
+
epochSeconds: 0,
|
|
187
|
+
}),
|
|
188
|
+
"Before 2024-01-01",
|
|
189
|
+
);
|
|
190
|
+
assert.equal(
|
|
191
|
+
searchTokenLabel({
|
|
192
|
+
type: "after",
|
|
193
|
+
raw: "after:2024-01-01",
|
|
194
|
+
value: "2024-01-01",
|
|
195
|
+
epochSeconds: 0,
|
|
196
|
+
}),
|
|
197
|
+
"After 2024-01-01",
|
|
198
|
+
);
|
|
199
|
+
assert.equal(
|
|
200
|
+
searchTokenLabel({
|
|
201
|
+
type: "in",
|
|
202
|
+
raw: "in:archive",
|
|
203
|
+
value: "archive",
|
|
204
|
+
mailboxId: "mailbox-1",
|
|
205
|
+
}),
|
|
206
|
+
"In: archive",
|
|
207
|
+
);
|
|
208
|
+
assert.equal(
|
|
209
|
+
searchTokenLabel({
|
|
210
|
+
type: "account",
|
|
211
|
+
raw: "account:work",
|
|
212
|
+
value: "work",
|
|
213
|
+
accountId: "account-1",
|
|
214
|
+
}),
|
|
215
|
+
"Account: work",
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter-token parser for the mail search field (#428). Recognizes a fixed set
|
|
3
|
+
* of `word:value` tokens typed inline with the free-text query and turns them
|
|
4
|
+
* into removable chips. Only tokens the search APIs can actually honor are
|
|
5
|
+
* recognized — everything else is left untouched as ordinary free-text words.
|
|
6
|
+
*
|
|
7
|
+
* `in:<mailbox>` and `account:<name>` resolve against a name index the caller
|
|
8
|
+
* supplies via `SearchTokenContext` (built from the loaded mailboxes/accounts —
|
|
9
|
+
* see `lib/search-token-index.ts`). An unresolvable name (no index, or no
|
|
10
|
+
* match in it) is left as free text rather than becoming a chip for a filter
|
|
11
|
+
* that can't actually apply.
|
|
12
|
+
*
|
|
13
|
+
* Pure functions only: no React, no fetch — the name index is injected, not
|
|
14
|
+
* fetched here. `MailListHeader` renders the chips and the callers that issue
|
|
15
|
+
* search requests (`MailboxPane`, `DailyBrief`, `FlaggedList`,
|
|
16
|
+
* `useSemanticSearch`) apply the parsed tokens to the params each engine's API
|
|
17
|
+
* actually supports.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type SearchToken =
|
|
21
|
+
| { type: "from"; raw: string; value: string }
|
|
22
|
+
| { type: "hasAttachment"; raw: string }
|
|
23
|
+
| { type: "isUnread"; raw: string }
|
|
24
|
+
| { type: "before"; raw: string; value: string; epochSeconds: number }
|
|
25
|
+
| { type: "after"; raw: string; value: string; epochSeconds: number }
|
|
26
|
+
| { type: "in"; raw: string; value: string; mailboxId: string }
|
|
27
|
+
| { type: "account"; raw: string; value: string; accountId: string };
|
|
28
|
+
|
|
29
|
+
export interface ParsedSearchQuery {
|
|
30
|
+
/** The query text with every recognized token removed, whitespace collapsed. */
|
|
31
|
+
freeText: string;
|
|
32
|
+
tokens: SearchToken[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Name lookups for resolving `in:`/`account:` tokens; see module doc. */
|
|
36
|
+
export interface SearchTokenContext {
|
|
37
|
+
/** Mailbox name (fullPath or its last segment, lower-cased) -> mailboxId. */
|
|
38
|
+
mailboxesByName?: ReadonlyMap<string, string>;
|
|
39
|
+
/** Account name (email or its local-part, lower-cased) -> accountId. */
|
|
40
|
+
accountsByName?: ReadonlyMap<string, string>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
44
|
+
|
|
45
|
+
function parseDateToken(
|
|
46
|
+
type: "before" | "after",
|
|
47
|
+
raw: string,
|
|
48
|
+
value: string,
|
|
49
|
+
): SearchToken | undefined {
|
|
50
|
+
if (!DATE_PATTERN.test(value)) return undefined;
|
|
51
|
+
const epochMs = Date.parse(`${value}T00:00:00Z`);
|
|
52
|
+
if (Number.isNaN(epochMs)) return undefined;
|
|
53
|
+
return { type, raw, value, epochSeconds: Math.floor(epochMs / 1000) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseWord(
|
|
57
|
+
word: string,
|
|
58
|
+
context: SearchTokenContext,
|
|
59
|
+
): SearchToken | undefined {
|
|
60
|
+
const lower = word.toLowerCase();
|
|
61
|
+
if (lower.startsWith("from:") && word.length > 5) {
|
|
62
|
+
return { type: "from", raw: word, value: word.slice(5) };
|
|
63
|
+
}
|
|
64
|
+
if (lower === "has:attachment") {
|
|
65
|
+
return { type: "hasAttachment", raw: word };
|
|
66
|
+
}
|
|
67
|
+
if (lower === "is:unread") {
|
|
68
|
+
return { type: "isUnread", raw: word };
|
|
69
|
+
}
|
|
70
|
+
if (lower.startsWith("before:") && word.length > 7) {
|
|
71
|
+
return parseDateToken("before", word, word.slice(7));
|
|
72
|
+
}
|
|
73
|
+
if (lower.startsWith("after:") && word.length > 6) {
|
|
74
|
+
return parseDateToken("after", word, word.slice(6));
|
|
75
|
+
}
|
|
76
|
+
if (lower.startsWith("in:") && word.length > 3) {
|
|
77
|
+
const value = word.slice(3);
|
|
78
|
+
const mailboxId = context.mailboxesByName?.get(value.toLowerCase());
|
|
79
|
+
if (!mailboxId) return undefined;
|
|
80
|
+
return { type: "in", raw: word, value, mailboxId };
|
|
81
|
+
}
|
|
82
|
+
if (lower.startsWith("account:") && word.length > 8) {
|
|
83
|
+
const value = word.slice(8);
|
|
84
|
+
const accountId = context.accountsByName?.get(value.toLowerCase());
|
|
85
|
+
if (!accountId) return undefined;
|
|
86
|
+
return { type: "account", raw: word, value, accountId };
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Split a query into recognized filter tokens and the remaining free text. */
|
|
92
|
+
export function parseSearchTokens(
|
|
93
|
+
query: string,
|
|
94
|
+
context: SearchTokenContext = {},
|
|
95
|
+
): ParsedSearchQuery {
|
|
96
|
+
const words = query.split(/\s+/).filter((w) => w.length > 0);
|
|
97
|
+
const tokens: SearchToken[] = [];
|
|
98
|
+
const freeWords: string[] = [];
|
|
99
|
+
for (const word of words) {
|
|
100
|
+
const token = parseWord(word, context);
|
|
101
|
+
if (token) tokens.push(token);
|
|
102
|
+
else freeWords.push(word);
|
|
103
|
+
}
|
|
104
|
+
return { freeText: freeWords.join(" "), tokens };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Remove one parsed token's raw text from the original query string. */
|
|
108
|
+
export function removeSearchToken(query: string, token: SearchToken): string {
|
|
109
|
+
return query
|
|
110
|
+
.split(/\s+/)
|
|
111
|
+
.filter((w) => w.length > 0 && w !== token.raw)
|
|
112
|
+
.join(" ");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Human label for the removable chip. */
|
|
116
|
+
export function searchTokenLabel(token: SearchToken): string {
|
|
117
|
+
switch (token.type) {
|
|
118
|
+
case "from":
|
|
119
|
+
return `From: ${token.value}`;
|
|
120
|
+
case "hasAttachment":
|
|
121
|
+
return "Has attachment";
|
|
122
|
+
case "isUnread":
|
|
123
|
+
return "Unread";
|
|
124
|
+
case "before":
|
|
125
|
+
return `Before ${token.value}`;
|
|
126
|
+
case "after":
|
|
127
|
+
return `After ${token.value}`;
|
|
128
|
+
case "in":
|
|
129
|
+
return `In: ${token.value}`;
|
|
130
|
+
case "account":
|
|
131
|
+
return `Account: ${token.value}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { Telemetry } from "./telemetry";
|
|
3
|
+
import { noopTelemetry } from "./telemetry";
|
|
4
|
+
|
|
5
|
+
export const TelemetryContext = createContext<Telemetry>(noopTelemetry);
|
|
6
|
+
|
|
7
|
+
export function useTelemetry(): Telemetry {
|
|
8
|
+
return useContext(TelemetryContext);
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Telemetry {
|
|
2
|
+
recordPageView(path: string): void;
|
|
3
|
+
recordError(error: Error, context?: Record<string, string>): void;
|
|
4
|
+
recordEvent(name: string, attributes?: Record<string, string>): void;
|
|
5
|
+
recordTiming(
|
|
6
|
+
name: string,
|
|
7
|
+
durationMs: number,
|
|
8
|
+
attributes?: Record<string, string>,
|
|
9
|
+
): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const noopTelemetry: Telemetry = {
|
|
13
|
+
recordPageView: () => undefined,
|
|
14
|
+
recordError: () => undefined,
|
|
15
|
+
recordEvent: () => undefined,
|
|
16
|
+
recordTiming: () => undefined,
|
|
17
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User theme preference — persistent, immediate-apply.
|
|
3
|
+
*
|
|
4
|
+
* "system" (default) follows the OS prefers-color-scheme media query.
|
|
5
|
+
* "light" / "dark" override it and survive page reloads via localStorage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type ThemePreference = "system" | "light" | "dark";
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY = "remit.theme";
|
|
11
|
+
const DARK_CLASS = "dark";
|
|
12
|
+
|
|
13
|
+
/** Read the stored preference; defaults to "system" if not set. */
|
|
14
|
+
export function getThemePreference(): ThemePreference {
|
|
15
|
+
if (typeof localStorage === "undefined") return "system";
|
|
16
|
+
return (
|
|
17
|
+
(localStorage.getItem(STORAGE_KEY) as ThemePreference | null) ?? "system"
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Apply a preference to <html> immediately, without persisting. */
|
|
22
|
+
export function applyPreference(pref: ThemePreference): void {
|
|
23
|
+
if (typeof document === "undefined") return;
|
|
24
|
+
const isDark =
|
|
25
|
+
pref === "dark" ||
|
|
26
|
+
(pref === "system" &&
|
|
27
|
+
typeof window !== "undefined" &&
|
|
28
|
+
typeof window.matchMedia === "function" &&
|
|
29
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
30
|
+
document.documentElement.classList.toggle(DARK_CLASS, isDark);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Persist the preference and apply it immediately.
|
|
35
|
+
* Passing "system" removes the key so the default kicks in.
|
|
36
|
+
*/
|
|
37
|
+
export function setThemePreference(pref: ThemePreference): void {
|
|
38
|
+
if (typeof localStorage !== "undefined") {
|
|
39
|
+
if (pref === "system") {
|
|
40
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
41
|
+
} else {
|
|
42
|
+
localStorage.setItem(STORAGE_KEY, pref);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
applyPreference(pref);
|
|
46
|
+
}
|
package/src/lib/theme.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { applyPreference, getThemePreference } from "./theme-preference.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* installThemeSync — call once at app startup (before first paint).
|
|
5
|
+
*
|
|
6
|
+
* Reads the stored preference (see theme-preference.ts) and applies it.
|
|
7
|
+
* The matchMedia listener stays attached for the whole session and re-reads
|
|
8
|
+
* the *live* preference on each OS change: while "system" it follows the OS,
|
|
9
|
+
* while "light"/"dark" it is a no-op. This means switching System → explicit
|
|
10
|
+
* → System within one session resumes OS-following immediately, with no
|
|
11
|
+
* reload required (the listener is never detached).
|
|
12
|
+
*/
|
|
13
|
+
export const installThemeSync = (): void => {
|
|
14
|
+
if (typeof window === "undefined") return;
|
|
15
|
+
|
|
16
|
+
applyPreference(getThemePreference());
|
|
17
|
+
|
|
18
|
+
if (typeof window.matchMedia !== "function") return;
|
|
19
|
+
|
|
20
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
21
|
+
const handler = (): void => {
|
|
22
|
+
// applyPreference only reacts to the OS when the live preference is
|
|
23
|
+
// "system"; an explicit light/dark choice makes this a no-op.
|
|
24
|
+
applyPreference(getThemePreference());
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (typeof media.addEventListener === "function") {
|
|
28
|
+
media.addEventListener("change", handler);
|
|
29
|
+
} else {
|
|
30
|
+
// Safari <14 fallback
|
|
31
|
+
media.addListener(handler);
|
|
32
|
+
}
|
|
33
|
+
};
|
package/src/lib/utils.ts
ADDED
package/src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Default distributor entry: composes the app shell with the combined auth
|
|
2
|
+
// provider (Cognito + better-auth, selected at runtime by `config.js`). This is
|
|
3
|
+
// what `@remit/web-client-dist` ships — one artifact for every deployment. A
|
|
4
|
+
// single-provider deployment writes its own entry against the primitives and
|
|
5
|
+
// omits the shell it does not use; see `harness/` for the reference build.
|
|
6
|
+
import "./auth/cognito/cognito.css";
|
|
7
|
+
import { combinedAuthProvider } from "./auth/combined-provider";
|
|
8
|
+
import { mountApp } from "./shell";
|
|
9
|
+
|
|
10
|
+
mountApp({ authProvider: combinedAuthProvider });
|