@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.
@@ -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 { isBriefRoute, isFlaggedRoute, isOutboxRoute } from "@/lib/mail-route";
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` is a load-once seed for the input and a one-directional write
90
- // target. The debounced local value is the source of truth and drives the
91
- // search API; it is mirrored back to the URL but the URL is never read into
92
- // state after mount.
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
- // Mirror the debounced search into the URL so links are shareable and a
100
- // refresh restores the query. One-directional: the URL is never read back
101
- // into state, so there is no sync loop.
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 (debouncedSearchInput === searchQueryRef.current) return;
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 === debouncedSearchInput;
156
+ (prev as { q?: string }).q === committedQuery;
117
157
  return {
118
158
  ...prev,
119
- q: debouncedSearchInput || undefined,
120
- ...(debouncedSearchInput && !queryAlreadyActive
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
- }, [debouncedSearchInput, navigate]);
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
- // Debounced local value is the source of truth for search; it is
254
- // mirrored to the URL one-directionally for shareable links.
255
- searchQuery: debouncedSearchInput,
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,