@remit/web-client 0.0.86 → 0.0.87

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.
Files changed (69) hide show
  1. package/package.json +1 -1
  2. package/src/auth/BetterAuthShell.tsx +2 -2
  3. package/src/components/compose/AddressField.tsx +60 -77
  4. package/src/components/mail/AutoMovedIndicator.tsx +7 -9
  5. package/src/components/mail/BriefPane.tsx +4 -2
  6. package/src/components/mail/DailyBrief.selection.test.ts +49 -0
  7. package/src/components/mail/DailyBrief.tsx +484 -76
  8. package/src/components/mail/FlaggedList.tsx +3 -2
  9. package/src/components/mail/FlaggedPane.tsx +4 -2
  10. package/src/components/mail/MailListHeader.tsx +155 -34
  11. package/src/components/mail/MailViewChrome.tsx +18 -1
  12. package/src/components/mail/MailboxPane.tsx +55 -20
  13. package/src/components/mail/MessageCard.tsx +0 -1
  14. package/src/components/mail/MessageList.tsx +70 -15
  15. package/src/components/mail/MessageListItem.tsx +4 -21
  16. package/src/components/mail/MessageRow.tsx +13 -31
  17. package/src/components/mail/SwipeableMessageRow.tsx +13 -6
  18. package/src/components/mail/ThreadListInteraction.tsx +51 -6
  19. package/src/components/mail/organize/OrganizeRuleEditor.tsx +57 -2
  20. package/src/components/mail/organize/SearchFilterDialog.render.test.ts +13 -2
  21. package/src/components/mail/organize/SearchFilterDialog.tsx +3 -1
  22. package/src/components/mail/organize/SearchFilterEditor.render.test.ts +37 -3
  23. package/src/components/mail/organize/SearchFilterEditor.tsx +8 -2
  24. package/src/components/mail/organize/smart-organize.stories.tsx +86 -5
  25. package/src/components/mail/useModifierSelect.render.test.ts +247 -0
  26. package/src/components/mail/useModifierSelect.ts +109 -0
  27. package/src/components/onboarding/OnboardingWizard.tsx +53 -9
  28. package/src/components/settings/AccountFormPanel.tsx +10 -5
  29. package/src/hooks/bulk-chunking.render.test.ts +157 -0
  30. package/src/hooks/useApplyLabel.ts +6 -3
  31. package/src/hooks/useClauseSuggestions.ts +57 -0
  32. package/src/hooks/useDeleteMessages.ts +6 -3
  33. package/src/hooks/useFollowFocusOpen.render.test.ts +155 -0
  34. package/src/hooks/useFollowFocusOpen.ts +81 -0
  35. package/src/hooks/useInitialSyncProgress.render.test.ts +280 -0
  36. package/src/hooks/useInitialSyncProgress.ts +134 -0
  37. package/src/hooks/useListCursor.ts +36 -5
  38. package/src/hooks/useMarkAsRead.ts +6 -3
  39. package/src/hooks/useMoveMessages.ts +6 -3
  40. package/src/hooks/useRulePreview.ts +11 -1
  41. package/src/hooks/useSearchFilterSeed.render.test.ts +50 -15
  42. package/src/hooks/useSearchFilterSeed.ts +21 -3
  43. package/src/hooks/useSearchSuggestions.ts +126 -0
  44. package/src/hooks/useSelectedSubjects.ts +0 -0
  45. package/src/hooks/useSemanticSearch.ts +26 -7
  46. package/src/hooks/useThreadActions.ts +11 -2
  47. package/src/lib/brief.test.ts +67 -0
  48. package/src/lib/brief.ts +11 -1
  49. package/src/lib/bulk-actions.ts +29 -0
  50. package/src/lib/drafts.ts +1 -1
  51. package/src/lib/organize/clause-suggestions.test.ts +113 -0
  52. package/src/lib/organize/clause-suggestions.ts +90 -0
  53. package/src/lib/organize/property-prefill.test.ts +173 -0
  54. package/src/lib/organize/property-prefill.ts +151 -0
  55. package/src/lib/organize/rule-model.test.ts +57 -0
  56. package/src/lib/organize/rule-model.ts +110 -31
  57. package/src/lib/organize/search-to-rule.ts +12 -4
  58. package/src/lib/organize/sender-fallback.ts +11 -1
  59. package/src/lib/search-result.ts +2 -0
  60. package/src/lib/search-suggestions.test.ts +249 -0
  61. package/src/lib/search-suggestions.ts +296 -0
  62. package/src/lib/search-token-index.test.ts +99 -0
  63. package/src/lib/search-token-index.ts +67 -1
  64. package/src/lib/search-tokens.test.ts +236 -0
  65. package/src/lib/search-tokens.ts +303 -36
  66. package/src/lib/thread-cache.ts +1 -1
  67. package/src/lib/thread-search-tokens.test.ts +193 -0
  68. package/src/lib/thread-search-tokens.ts +148 -0
  69. package/src/routes/onboarding.tsx +9 -4
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The parsed search tokens as `threadOperationsSearchThreads` parameters, plus
3
+ * the tokens that request cannot carry.
4
+ *
5
+ * A token the request drops does not narrow anything, so the mailbox answers a
6
+ * weaker question than the one that was asked and returns mail the user
7
+ * excluded — `is:starred` handing back unstarred mail. Every token is therefore
8
+ * accounted for exactly once: either the request carries it, or it comes back
9
+ * as residue for the caller to apply over the returned rows
10
+ * (`matchesSearchTokens`).
11
+ *
12
+ * Membership is decided by reading the request back rather than by tracking
13
+ * which branch set what: a token is carried when the request that will be sent
14
+ * asks for exactly what the token asks for. That covers the cases where one
15
+ * parameter has to answer for several tokens — the endpoint takes a single
16
+ * `from`, a single `subject` and one boolean per state, so a second `from:` or
17
+ * a contradicting `is:read` cannot be expressed and becomes residue.
18
+ *
19
+ * The inbox chips own the parameters they set: a chip is a control on screen,
20
+ * a token is text in a field, and where they disagree the visible control wins
21
+ * and the token drops to residue (where, contradicting the chip, it matches
22
+ * nothing — which is the honest answer to a contradiction).
23
+ */
24
+
25
+ import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
26
+ import { matchesSearchTokens, toThreadRowData } from "./brief.js";
27
+ import type { InboxFilterParams, ThreadSearchQuery } from "./inbox-filters.js";
28
+ import type { SearchToken } from "./search-tokens.js";
29
+
30
+ /** The search parameters the tokens themselves set. */
31
+ export type ThreadSearchTokenParams = Pick<
32
+ ThreadSearchQuery,
33
+ "from" | "subject" | "category" | "unread" | "starred" | "attachments"
34
+ >;
35
+
36
+ export interface ThreadSearchTokens {
37
+ /** What the tokens add to the request. */
38
+ params: ThreadSearchTokenParams;
39
+ /** The tokens the request does not carry; apply these over the rows. */
40
+ residual: SearchToken[];
41
+ }
42
+
43
+ /** The first value wins: the endpoint takes one value per parameter. */
44
+ const firstValue = <T>(values: T[]): T | undefined => values[0];
45
+
46
+ const paramsFromTokens = (
47
+ tokens: readonly SearchToken[],
48
+ ): ThreadSearchTokenParams => {
49
+ const from = firstValue(
50
+ tokens.filter((t) => t.type === "from").map((t) => t.value),
51
+ );
52
+ const subject = firstValue(
53
+ tokens.filter((t) => t.type === "subject").map((t) => t.value),
54
+ );
55
+ const category = firstValue(
56
+ tokens.filter((t) => t.type === "category").map((t) => t.category),
57
+ );
58
+ const unread = firstValue(
59
+ tokens
60
+ .filter((t) => t.type === "isUnread" || t.type === "isRead")
61
+ .map((t) => t.type === "isUnread"),
62
+ );
63
+ const starred = tokens.some((t) => t.type === "isStarred") ? true : undefined;
64
+ const attachments = tokens.some((t) => t.type === "hasAttachment")
65
+ ? true
66
+ : undefined;
67
+ return {
68
+ ...(from !== undefined ? { from } : {}),
69
+ ...(subject !== undefined ? { subject } : {}),
70
+ ...(category !== undefined ? { category: [category] } : {}),
71
+ ...(unread !== undefined ? { unread } : {}),
72
+ ...(starred !== undefined ? { starred } : {}),
73
+ ...(attachments !== undefined ? { attachments } : {}),
74
+ };
75
+ };
76
+
77
+ /** Whether the request being sent asks for exactly what this token asks for. */
78
+ const isCarried = (
79
+ request: ThreadSearchTokenParams,
80
+ token: SearchToken,
81
+ ): boolean => {
82
+ switch (token.type) {
83
+ case "from":
84
+ return request.from?.toLowerCase() === token.value.toLowerCase();
85
+ case "subject":
86
+ return request.subject?.toLowerCase() === token.value.toLowerCase();
87
+ case "category":
88
+ return (
89
+ request.category?.length === 1 && request.category[0] === token.category
90
+ );
91
+ case "isUnread":
92
+ return request.unread === true;
93
+ case "isRead":
94
+ return request.unread === false;
95
+ case "isStarred":
96
+ return request.starred === true;
97
+ case "hasAttachment":
98
+ return request.attachments === true;
99
+ // No parameter on this endpoint at all: dates, the account and the
100
+ // mailbox are the caller's to apply.
101
+ case "before":
102
+ case "after":
103
+ case "in":
104
+ case "account":
105
+ return false;
106
+ }
107
+ };
108
+
109
+ /**
110
+ * Split the tokens into the parameters the thread search takes and the residue
111
+ * the caller has to apply itself. `chipParams` are the parameters already set
112
+ * by the inbox filter chips, which win where they overlap.
113
+ */
114
+ export function threadSearchTokens(
115
+ tokens: readonly SearchToken[],
116
+ chipParams: InboxFilterParams = {},
117
+ ): ThreadSearchTokens {
118
+ const params = paramsFromTokens(tokens);
119
+ const request: ThreadSearchTokenParams = { ...params, ...chipParams };
120
+ return {
121
+ params,
122
+ residual: tokens.filter((token) => !isCarried(request, token)),
123
+ };
124
+ }
125
+
126
+ /**
127
+ * The rows that satisfy every residual token. Identity when there is none.
128
+ *
129
+ * `accountId` names the account the rows belong to. A per-mailbox listing does
130
+ * not carry one on the row — the mailbox already answers it — and a row whose
131
+ * account is unknown cannot satisfy `account:`, so it drops rather than showing
132
+ * under a filter nothing verified.
133
+ */
134
+ export function applyResidualTokens(
135
+ threads: RemitImapThreadMessageResponse[],
136
+ residual: readonly SearchToken[],
137
+ accountId?: string,
138
+ ): RemitImapThreadMessageResponse[] {
139
+ if (residual.length === 0) return threads;
140
+ const tokens = [...residual];
141
+ return threads.filter((thread) => {
142
+ const row = toThreadRowData(thread);
143
+ return matchesSearchTokens(
144
+ { ...row, accountId: row.accountId ?? accountId },
145
+ tokens,
146
+ );
147
+ });
148
+ }
@@ -1,5 +1,5 @@
1
1
  import { useQueryClient } from "@tanstack/react-query";
2
- import { createFileRoute, useNavigate } from "@tanstack/react-router";
2
+ import { createFileRoute } from "@tanstack/react-router";
3
3
  import { useCallback } from "react";
4
4
  import { OnboardingWizard } from "@/components/onboarding/OnboardingWizard";
5
5
  import { completeOnboarding } from "@/lib/onboarding-completion";
@@ -16,19 +16,24 @@ export const Route = createFileRoute("/onboarding")({
16
16
  });
17
17
 
18
18
  function OnboardingPage() {
19
- const navigate = useNavigate();
20
19
  const telemetry = useTelemetry();
21
20
  const queryClient = useQueryClient();
22
21
 
22
+ // A client-side navigation carries the query cache with it, and the /mail
23
+ // first-run guard reads config from that cache — a stale zero-account entry
24
+ // bounces the user straight back into the wizard. A document load starts
25
+ // from an empty cache, so the guard sees the account that was just created.
23
26
  const handleComplete = useCallback(
24
27
  (_accountId: string) => {
25
28
  void completeOnboarding({
26
29
  queryClient,
27
30
  recordCompleted: () => telemetry.recordEvent("onboarding.completed"),
28
- navigateToInbox: () => void navigate({ to: "/mail" }),
31
+ navigateToInbox: () => {
32
+ window.location.assign("/mail");
33
+ },
29
34
  });
30
35
  },
31
- [navigate, telemetry, queryClient],
36
+ [telemetry, queryClient],
32
37
  );
33
38
 
34
39
  return <OnboardingWizard skipWelcome={false} onComplete={handleComplete} />;