@remit/web-client 0.0.17 → 0.0.19
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/components/layout/MailTopBar.tsx +27 -1
- package/src/components/mail/DailyBrief.tsx +4 -2
- package/src/components/mail/FlaggedList.tsx +4 -2
- package/src/components/mail/MailListHeader.tsx +45 -27
- package/src/components/mail/MailSidebarAdapter.tsx +11 -2
- package/src/components/mail/MailViewChrome.tsx +7 -0
- package/src/components/mail/MailboxPane.tsx +19 -12
- package/src/components/mail/OutboxPane.tsx +67 -8
- package/src/hooks/useSearchScope.ts +49 -0
- package/src/hooks/useSearchTokenContext.ts +28 -0
- package/src/hooks/useSemanticSearch.ts +13 -9
- package/src/lib/mail-context.ts +8 -1
- package/src/lib/mail-route.ts +17 -0
- package/src/lib/outbox-search.test.ts +75 -0
- package/src/lib/outbox-search.ts +58 -0
- package/src/lib/search-scope.test.ts +104 -0
- package/src/lib/search-scope.ts +114 -0
- package/src/lib/search-view.test.ts +205 -0
- package/src/lib/search-view.ts +54 -0
- package/src/routes/mail.tsx +57 -16
package/src/routes/mail.tsx
CHANGED
|
@@ -30,8 +30,18 @@ import { useMailboxNameIndex } from "@/hooks/useMailboxNameIndex";
|
|
|
30
30
|
import { useStaleAccountSync } from "@/hooks/useStaleAccountSync";
|
|
31
31
|
import { writeIntelligencePref } from "@/lib/intelligence-pref";
|
|
32
32
|
import { MailContext } from "@/lib/mail-context";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
isBriefRoute,
|
|
35
|
+
isFlaggedRoute,
|
|
36
|
+
isOutboxRoute,
|
|
37
|
+
mailViewKey,
|
|
38
|
+
} from "@/lib/mail-route";
|
|
34
39
|
import { buildAccountNameIndex } from "@/lib/search-token-index";
|
|
40
|
+
import {
|
|
41
|
+
committedSearchQuery,
|
|
42
|
+
searchInputForView,
|
|
43
|
+
shouldMirrorQuery,
|
|
44
|
+
} from "@/lib/search-view";
|
|
35
45
|
import "@/lib/client";
|
|
36
46
|
|
|
37
47
|
// `MailContext` / `useMailContext` live in `@/lib/mail-context` so the provider
|
|
@@ -86,19 +96,48 @@ function MailLayout() {
|
|
|
86
96
|
writeIntelligencePref(open);
|
|
87
97
|
}, []);
|
|
88
98
|
|
|
89
|
-
// URL `q`
|
|
90
|
-
// target
|
|
91
|
-
//
|
|
92
|
-
//
|
|
99
|
+
// Within one view, URL `q` seeds the input and is a one-directional write
|
|
100
|
+
// target: the debounced local value drives the search API and is mirrored
|
|
101
|
+
// back. Across views the URL wins again — see the view-change adjustment
|
|
102
|
+
// below and `lib/search-view.ts` (#47).
|
|
93
103
|
const [searchInput, setSearchInput] = useState(searchQuery);
|
|
94
104
|
const debouncedSearchInput = useDebouncedValue(searchInput, 200);
|
|
105
|
+
const committedQuery = committedSearchQuery(
|
|
106
|
+
searchInput,
|
|
107
|
+
debouncedSearchInput,
|
|
108
|
+
);
|
|
95
109
|
|
|
96
110
|
const searchQueryRef = useRef(searchQuery);
|
|
97
111
|
searchQueryRef.current = searchQuery;
|
|
98
112
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
113
|
+
// Search is a mode of the view it was typed in, so leaving that view re-seeds
|
|
114
|
+
// the field from wherever we land (#47): empty when the sidebar dropped `q`
|
|
115
|
+
// (a folder switch starts that folder's search fresh), and the carried query
|
|
116
|
+
// when the top bar's scope chip was removed and sent the user to the brief to
|
|
117
|
+
// search everything. Views that differ only in search params — opening a
|
|
118
|
+
// result, mirroring `q` — are the same view, so in-flight typing survives
|
|
119
|
+
// them (`searchInputForView`).
|
|
120
|
+
//
|
|
121
|
+
// Adjusted during render, not in an effect. This is React's documented
|
|
122
|
+
// "adjusting state when a prop changes" pattern: both updates are to this
|
|
123
|
+
// component's own state and are guarded by a changed value, so React re-runs
|
|
124
|
+
// the render before committing and nothing is painted with the stale query.
|
|
125
|
+
// An effect would commit one frame carrying the previous view's text, which
|
|
126
|
+
// the mirror below then has to be defended against.
|
|
127
|
+
const viewKey = useRouterState({ select: (s) => mailViewKey(s.matches) });
|
|
128
|
+
const [searchViewKey, setSearchViewKey] = useState(viewKey);
|
|
129
|
+
if (searchViewKey !== viewKey) {
|
|
130
|
+
const seeded = searchInputForView(searchViewKey, viewKey, searchQuery);
|
|
131
|
+
setSearchViewKey(viewKey);
|
|
132
|
+
if (seeded !== undefined) setSearchInput(seeded);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Mirror the settled search into the URL so links are shareable and a
|
|
136
|
+
// refresh restores the query. Within a view this is one-directional — the
|
|
137
|
+
// URL is not read back into state, so there is no sync loop — and it writes
|
|
138
|
+
// only once the debounce agrees with the field, so a query arriving by URL
|
|
139
|
+
// is never overwritten mid-debounce, and the query the user just left behind
|
|
140
|
+
// is never written onto the view they landed on (`shouldMirrorQuery`).
|
|
102
141
|
// When a query *goes* active, also strip selectedMessageId so the reading
|
|
103
142
|
// pane closes (#539): an open message from the pre-search list is not
|
|
104
143
|
// meaningful in the search result set. Only strip on that transition though —
|
|
@@ -108,16 +147,17 @@ function MailLayout() {
|
|
|
108
147
|
// the row shows before the debounce settles, so this mirror can land just
|
|
109
148
|
// after the open and close it again.
|
|
110
149
|
useEffect(() => {
|
|
111
|
-
if (
|
|
150
|
+
if (!shouldMirrorQuery(searchInput, committedQuery, searchQueryRef.current))
|
|
151
|
+
return;
|
|
112
152
|
navigate({
|
|
113
153
|
to: ".",
|
|
114
154
|
search: (prev) => {
|
|
115
155
|
const queryAlreadyActive =
|
|
116
|
-
(prev as { q?: string }).q ===
|
|
156
|
+
(prev as { q?: string }).q === committedQuery;
|
|
117
157
|
return {
|
|
118
158
|
...prev,
|
|
119
|
-
q:
|
|
120
|
-
...(
|
|
159
|
+
q: committedQuery || undefined,
|
|
160
|
+
...(committedQuery && !queryAlreadyActive
|
|
121
161
|
? {
|
|
122
162
|
selectedMessageId: undefined,
|
|
123
163
|
selectedThreadId: undefined,
|
|
@@ -128,7 +168,7 @@ function MailLayout() {
|
|
|
128
168
|
},
|
|
129
169
|
replace: true,
|
|
130
170
|
});
|
|
131
|
-
}, [
|
|
171
|
+
}, [searchInput, committedQuery, navigate]);
|
|
132
172
|
|
|
133
173
|
const {
|
|
134
174
|
data: config,
|
|
@@ -250,10 +290,11 @@ function MailLayout() {
|
|
|
250
290
|
accounts,
|
|
251
291
|
mailboxNameIndex,
|
|
252
292
|
accountNameIndex,
|
|
253
|
-
//
|
|
254
|
-
// mirrored to the URL
|
|
255
|
-
searchQuery:
|
|
293
|
+
// The committed local value is the source of truth for search; it is
|
|
294
|
+
// mirrored to the URL for shareable links.
|
|
295
|
+
searchQuery: committedQuery,
|
|
256
296
|
searchInput,
|
|
297
|
+
searchViewKey: viewKey,
|
|
257
298
|
onSearchChange: handleSearchChange,
|
|
258
299
|
onSearchClear: handleSearchClear,
|
|
259
300
|
onSearchClearQuery: handleSearchClearQuery,
|