@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.
- package/package.json +1 -1
- package/src/auth/BetterAuthShell.tsx +2 -2
- package/src/components/compose/AddressField.tsx +60 -77
- package/src/components/mail/AutoMovedIndicator.tsx +7 -9
- package/src/components/mail/BriefPane.tsx +4 -2
- package/src/components/mail/DailyBrief.selection.test.ts +49 -0
- package/src/components/mail/DailyBrief.tsx +484 -76
- package/src/components/mail/FlaggedList.tsx +3 -2
- package/src/components/mail/FlaggedPane.tsx +4 -2
- package/src/components/mail/MailListHeader.tsx +155 -34
- package/src/components/mail/MailViewChrome.tsx +18 -1
- package/src/components/mail/MailboxPane.tsx +55 -20
- package/src/components/mail/MessageCard.tsx +0 -1
- package/src/components/mail/MessageList.tsx +70 -15
- package/src/components/mail/MessageListItem.tsx +4 -21
- package/src/components/mail/MessageRow.tsx +13 -31
- package/src/components/mail/SwipeableMessageRow.tsx +13 -6
- package/src/components/mail/ThreadListInteraction.tsx +51 -6
- package/src/components/mail/organize/OrganizeRuleEditor.tsx +57 -2
- package/src/components/mail/organize/SearchFilterDialog.render.test.ts +13 -2
- package/src/components/mail/organize/SearchFilterDialog.tsx +3 -1
- package/src/components/mail/organize/SearchFilterEditor.render.test.ts +37 -3
- package/src/components/mail/organize/SearchFilterEditor.tsx +8 -2
- package/src/components/mail/organize/smart-organize.stories.tsx +86 -5
- package/src/components/mail/useModifierSelect.render.test.ts +247 -0
- package/src/components/mail/useModifierSelect.ts +109 -0
- package/src/components/onboarding/OnboardingWizard.tsx +53 -9
- package/src/components/settings/AccountFormPanel.tsx +10 -5
- package/src/hooks/bulk-chunking.render.test.ts +157 -0
- package/src/hooks/useApplyLabel.ts +6 -3
- package/src/hooks/useClauseSuggestions.ts +57 -0
- package/src/hooks/useDeleteMessages.ts +6 -3
- package/src/hooks/useFollowFocusOpen.render.test.ts +155 -0
- package/src/hooks/useFollowFocusOpen.ts +81 -0
- package/src/hooks/useInitialSyncProgress.render.test.ts +280 -0
- package/src/hooks/useInitialSyncProgress.ts +134 -0
- package/src/hooks/useListCursor.ts +36 -5
- package/src/hooks/useMarkAsRead.ts +6 -3
- package/src/hooks/useMoveMessages.ts +6 -3
- package/src/hooks/useRulePreview.ts +11 -1
- package/src/hooks/useSearchFilterSeed.render.test.ts +50 -15
- package/src/hooks/useSearchFilterSeed.ts +21 -3
- package/src/hooks/useSearchSuggestions.ts +126 -0
- package/src/hooks/useSelectedSubjects.ts +0 -0
- package/src/hooks/useSemanticSearch.ts +26 -7
- package/src/hooks/useThreadActions.ts +11 -2
- package/src/lib/brief.test.ts +67 -0
- package/src/lib/brief.ts +11 -1
- package/src/lib/bulk-actions.ts +29 -0
- package/src/lib/drafts.ts +1 -1
- package/src/lib/organize/clause-suggestions.test.ts +113 -0
- package/src/lib/organize/clause-suggestions.ts +90 -0
- package/src/lib/organize/property-prefill.test.ts +173 -0
- package/src/lib/organize/property-prefill.ts +151 -0
- package/src/lib/organize/rule-model.test.ts +57 -0
- package/src/lib/organize/rule-model.ts +110 -31
- package/src/lib/organize/search-to-rule.ts +12 -4
- package/src/lib/organize/sender-fallback.ts +11 -1
- package/src/lib/search-result.ts +2 -0
- package/src/lib/search-suggestions.test.ts +249 -0
- package/src/lib/search-suggestions.ts +296 -0
- package/src/lib/search-token-index.test.ts +99 -0
- package/src/lib/search-token-index.ts +67 -1
- package/src/lib/search-tokens.test.ts +236 -0
- package/src/lib/search-tokens.ts +303 -36
- package/src/lib/thread-cache.ts +1 -1
- package/src/lib/thread-search-tokens.test.ts +193 -0
- package/src/lib/thread-search-tokens.ts +148 -0
- 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
|
|
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: () =>
|
|
31
|
+
navigateToInbox: () => {
|
|
32
|
+
window.location.assign("/mail");
|
|
33
|
+
},
|
|
29
34
|
});
|
|
30
35
|
},
|
|
31
|
-
[
|
|
36
|
+
[telemetry, queryClient],
|
|
32
37
|
);
|
|
33
38
|
|
|
34
39
|
return <OnboardingWizard skipWelcome={false} onComplete={handleComplete} />;
|