@remit/web-client 0.0.202 → 0.0.204

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 (47) hide show
  1. package/package.json +1 -1
  2. package/spellcheck/vite-plugin-licence.test.ts +66 -18
  3. package/src/components/compose/ComposeSmtpMissingBanner.tsx +3 -6
  4. package/src/components/layout/ComposeFab.tsx +3 -5
  5. package/src/components/layout/MailTopBar.tsx +3 -4
  6. package/src/components/mail/BriefPane.tsx +14 -31
  7. package/src/components/mail/DailyBrief.tsx +9 -12
  8. package/src/components/mail/DraftsView.tsx +3 -17
  9. package/src/components/mail/FlaggedList.tsx +1 -1
  10. package/src/components/mail/FlaggedPane.tsx +14 -31
  11. package/src/components/mail/MailListHeader.tsx +10 -11
  12. package/src/components/mail/MailSidebarAdapter.tsx +4 -25
  13. package/src/components/mail/MailboxPane.tsx +21 -59
  14. package/src/components/mail/MessageActionMenu.tsx +4 -11
  15. package/src/components/mail/MessageList.tsx +9 -13
  16. package/src/components/mail/OutboxPane.tsx +9 -26
  17. package/src/components/mail/SelectionWizardHost.tsx +1 -1
  18. package/src/components/mail/SwipeableMessageRow.tsx +4 -15
  19. package/src/hooks/useCurrentMailboxName.ts +5 -9
  20. package/src/hooks/useSearchScope.ts +6 -12
  21. package/src/hooks/useSearchTokenContext.ts +3 -2
  22. package/src/hooks/useSemanticSearch.ts +3 -3
  23. package/src/lib/mail-route.ts +10 -0
  24. package/src/lib/search-scope.test.ts +45 -32
  25. package/src/lib/search-scope.ts +14 -19
  26. package/src/lib/search-view.test.ts +1 -1
  27. package/src/lib/search-view.ts +2 -2
  28. package/src/routes/mail/$mailboxId.tsx +1 -2
  29. package/src/routes/mail/brief.tsx +1 -2
  30. package/src/routes/mail/flagged.tsx +1 -2
  31. package/src/routes/mail/outbox.tsx +1 -2
  32. package/src/routes/mail.tsx +7 -3
  33. package/src/routing/browsed-list.ts +60 -9
  34. package/src/routing/compose.ts +2 -27
  35. package/src/routing/index.ts +40 -1
  36. package/src/routing/nav-selection.ts +24 -0
  37. package/src/routing/open-thread.ts +89 -2
  38. package/src/routing/outbox-draft.ts +27 -1
  39. package/src/{hooks/useSearchField.render.test.ts → routing/search-field.render.test.ts} +1 -1
  40. package/src/{hooks/useSearchField.ts → routing/search-field.ts} +1 -1
  41. package/src/{hooks → routing}/search-mirror-convergence.render.test.ts +1 -1
  42. package/src/{hooks → routing}/search-mirror-detail.render.test.ts +1 -1
  43. package/src/{hooks/useSearchMirror.ts → routing/search-mirror.ts} +2 -1
  44. package/src/routing/search-navigation.ts +44 -0
  45. package/src/routing/sections.ts +53 -0
  46. /package/src/{lib → routing}/wizard-history.test.ts +0 -0
  47. /package/src/{lib → routing}/wizard-history.ts +0 -0
@@ -33,7 +33,6 @@ import {
33
33
  type SearchResult,
34
34
  } from "@remit/ui";
35
35
  import { useInfiniteQuery } from "@tanstack/react-query";
36
- import { useNavigate } from "@tanstack/react-router";
37
36
  import {
38
37
  createContext,
39
38
  type ReactNode,
@@ -127,11 +126,13 @@ import {
127
126
  type OpenThreadTarget,
128
127
  type ReplyMode,
129
128
  replyToThread,
129
+ useCloseThread,
130
+ useGoToSection,
130
131
  useIsComposing,
131
132
  useIsReplying,
132
133
  useOpenCompose,
133
134
  useOpenReply,
134
- useRetainOpenPanels,
135
+ useOpenThread,
135
136
  } from "@/routing";
136
137
  import { MailViewChrome } from "./MailViewChrome";
137
138
 
@@ -270,8 +271,9 @@ function MailboxPaneProvider({
270
271
  thread,
271
272
  children,
272
273
  }: MailboxPaneProps) {
273
- const navigate = useNavigate();
274
- const retainPanels = useRetainOpenPanels();
274
+ const openThread = useOpenThread();
275
+ const closeThread = useCloseThread();
276
+ const goToSection = useGoToSection();
275
277
  const threadId = thread?.threadId;
276
278
  const pointedAtMessageId = thread?.messageId;
277
279
  const telemetry = useTelemetry();
@@ -470,26 +472,6 @@ function MailboxPaneProvider({
470
472
  // Esc unwinds one step at a time: an active selection first (handled by the
471
473
  // triage layer), then the open thread — which is a navigation up to the list,
472
474
  // so nothing is left mounted below it.
473
- const closeThread = useCallback(() => {
474
- navigate({
475
- to: "/mail/$mailboxId",
476
- params: { mailboxId },
477
- search: (prev) => prev,
478
- hash: retainPanels,
479
- });
480
- }, [mailboxId, navigate, retainPanels]);
481
-
482
- const handleOpenThread = useCallback(
483
- (target: OpenThreadTarget) => {
484
- navigate({
485
- to: "/mail/$mailboxId/$threadId/$messageId",
486
- params: { mailboxId, ...target },
487
- search: (prev) => prev,
488
- hash: retainPanels,
489
- });
490
- },
491
- [mailboxId, navigate, retainPanels],
492
- );
493
475
 
494
476
  const handleDeselectIfRemoved = useCallback(
495
477
  (removedIds: string[]) => {
@@ -716,13 +698,6 @@ function MailboxPaneProvider({
716
698
  intelligenceRef.current?.open();
717
699
  }, []);
718
700
 
719
- const goToRoute = useCallback(
720
- (to: "/mail/brief" | "/mail/flagged" | "/settings") => {
721
- navigate({ to });
722
- },
723
- [navigate],
724
- );
725
-
726
701
  const mailboxType = isDraftsMailbox
727
702
  ? "drafts"
728
703
  : archiveMailboxId === mailboxId
@@ -764,11 +739,11 @@ function MailboxPaneProvider({
764
739
  ? () => intelligenceRef.current?.toggle()
765
740
  : undefined,
766
741
  compose: openCompose,
767
- goBrief: () => goToRoute("/mail/brief"),
768
- goInbox: () => goToRoute("/mail/brief"),
769
- goSent: () => goToRoute("/mail/brief"),
770
- goFlagged: () => goToRoute("/mail/flagged"),
771
- goSettings: () => goToRoute("/settings"),
742
+ goBrief: () => goToSection("brief"),
743
+ goInbox: () => goToSection("brief"),
744
+ goSent: () => goToSection("brief"),
745
+ goFlagged: () => goToSection("flagged"),
746
+ goSettings: () => goToSection("settings"),
772
747
  },
773
748
  });
774
749
 
@@ -788,7 +763,7 @@ function MailboxPaneProvider({
788
763
  selectedMessageId,
789
764
  selectedThread,
790
765
  conversation,
791
- onOpenThread: handleOpenThread,
766
+ onOpenThread: openThread,
792
767
  threads,
793
768
  isLoading,
794
769
  isError,
@@ -885,8 +860,7 @@ function MailboxList() {
885
860
  const { searchQuery, searchInput, accounts, resultFolderIndex } =
886
861
  useMailContext();
887
862
  const tier = useLayoutTier();
888
- const navigate = useNavigate();
889
- const retainPanels = useRetainOpenPanels();
863
+ const openThread = useOpenThread();
890
864
 
891
865
  const listTitle = mailboxName ?? "Inbox";
892
866
  const preset = useMemo(() => inboxFilterConfig(), []);
@@ -970,26 +944,15 @@ function MailboxList() {
970
944
  result.threadId ??
971
945
  threads.find((thread) => thread.messageId === result.id)?.threadId;
972
946
  if (!threadId) return;
973
- navigate({
974
- to: "/mail/$mailboxId/$threadId/$messageId",
975
- // Both sections are scoped to this mailbox, so a result's own
976
- // mailbox is normally this one; keep reading it off the result so a
977
- // row can never open under a mailbox it does not belong to.
978
- params: {
979
- mailboxId: result.mailboxId ?? mailboxId,
980
- threadId,
981
- messageId: result.id,
982
- },
983
- // Commit the active query with the open so the debounced q-mirror —
984
- // which walks back up to the list when the query goes active — is
985
- // already satisfied and leaves the conversation alone. The *live*
986
- // `searchInput`: a row can be tapped before the debounce settles, when
987
- // the committed query is still empty.
988
- search: (prev) => ({ ...prev, q: searchInput || undefined }),
989
- hash: retainPanels,
990
- });
947
+ // Both sections are scoped to this mailbox, so a result's own mailbox is
948
+ // normally this one; keep reading it off the result so a row can never
949
+ // open under a mailbox it does not belong to.
950
+ openThread(
951
+ { threadId, messageId: result.id },
952
+ { mailboxId: result.mailboxId ?? mailboxId, query: searchInput },
953
+ );
991
954
  },
992
- [mailboxId, navigate, retainPanels, searchInput, threads],
955
+ [mailboxId, openThread, searchInput, threads],
993
956
  );
994
957
 
995
958
  // Drafts keep their own dedicated view (and header); they don't carry the
@@ -997,7 +960,6 @@ function MailboxList() {
997
960
  if (isDraftsMailbox && mailboxAccountId) {
998
961
  return (
999
962
  <DraftsView
1000
- mailboxId={mailboxId}
1001
963
  accountId={mailboxAccountId}
1002
964
  selectedMessageId={selectedMessageId}
1003
965
  imapThreads={threads}
@@ -3,7 +3,6 @@ import {
3
3
  threadDetailOperationsListThreadMessagesQueryKey,
4
4
  } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
5
5
  import { useMutation, useQueryClient } from "@tanstack/react-query";
6
- import { useNavigate } from "@tanstack/react-router";
7
6
  import {
8
7
  BadgeCheck,
9
8
  Check,
@@ -35,7 +34,7 @@ import {
35
34
  type ThreadMutationContext,
36
35
  threadListCacheKeys,
37
36
  } from "@/lib/thread-list-cache";
38
- import { useOpenThreadPath, useRetainOpenPanels } from "@/routing";
37
+ import { useCloseThread, useOpenThreadPath } from "@/routing";
39
38
  import { MoveToTrigger } from "./MoveToTrigger";
40
39
 
41
40
  interface MessageActionMenuProps {
@@ -85,8 +84,7 @@ export const MessageActionMenu = ({
85
84
  onToggleRaw,
86
85
  }: MessageActionMenuProps) => {
87
86
  const queryClient = useQueryClient();
88
- const navigate = useNavigate();
89
- const retainPanels = useRetainOpenPanels();
87
+ const closeThread = useCloseThread();
90
88
  const { pushError } = useErrorBanners();
91
89
  const selectedMessageId = useOpenThreadPath()?.messageId;
92
90
 
@@ -173,14 +171,9 @@ export const MessageActionMenu = ({
173
171
  (removedIds: string[]) => {
174
172
  if (!selectedMessageId) return;
175
173
  if (!removedIds.includes(selectedMessageId)) return;
176
- navigate({
177
- to: "/mail/$mailboxId",
178
- params: { mailboxId },
179
- search: (prev) => prev,
180
- hash: retainPanels,
181
- });
174
+ closeThread();
182
175
  },
183
- [selectedMessageId, mailboxId, navigate, retainPanels],
176
+ [selectedMessageId, closeThread],
184
177
  );
185
178
 
186
179
  const { deleteMessages, isPending: isDeleting } = useDeleteMessages({
@@ -13,7 +13,7 @@ import {
13
13
  useSelection,
14
14
  type Verb,
15
15
  } from "@remit/ui";
16
- import { useBlocker, useNavigate } from "@tanstack/react-router";
16
+ import { useBlocker } from "@tanstack/react-router";
17
17
  import { useVirtualizer } from "@tanstack/react-virtual";
18
18
  import { Search } from "lucide-react";
19
19
  import type { RefObject } from "react";
@@ -52,9 +52,12 @@ import { useListHeaderChrome } from "@/lib/list-header-chrome";
52
52
  import { listVerbRequest } from "@/lib/list-verb-request";
53
53
  import { resolveSelectionAccountScope } from "@/lib/selection-account-scope";
54
54
  import { shouldExitSelectionOnNavigate } from "@/lib/selection-mode";
55
- import { useSelectionWizard, useWizardStepValue } from "@/lib/wizard-history";
56
55
  import type { WizardSelectionMessage } from "@/lib/wizard-selection";
57
- import { useRetainOpenPanels } from "@/routing";
56
+ import {
57
+ useOpenThread,
58
+ useSelectionWizard,
59
+ useWizardStepValue,
60
+ } from "@/routing";
58
61
  import { DeleteConfirmDialog } from "./DeleteConfirmDialog";
59
62
  import { LabelApplyTrigger } from "./LabelApplyTrigger";
60
63
  import {
@@ -251,8 +254,7 @@ export const MessageList = ({
251
254
  hideHeader = false,
252
255
  }: MessageListProps) => {
253
256
  const parentRef = useRef<HTMLDivElement>(null);
254
- const navigate = useNavigate();
255
- const retainPanels = useRetainOpenPanels();
257
+ const openThread = useOpenThread();
256
258
  const isDesktop = useIsDesktop();
257
259
  const wizard = useSelectionWizard();
258
260
  const { verb: wizardVerb, start: startWizard, startFromSearch } = wizard;
@@ -655,15 +657,9 @@ export const MessageList = ({
655
657
  (thread) => thread.messageId === messageId,
656
658
  )?.threadId;
657
659
  if (!threadId) return;
658
- navigate({
659
- to: "/mail/$mailboxId/$threadId/$messageId",
660
- params: { mailboxId, threadId, messageId },
661
- search: (prev) => prev,
662
- hash: retainPanels,
663
- replace: options?.replace,
664
- });
660
+ openThread({ threadId, messageId }, { replace: options?.replace });
665
661
  },
666
- [navigate, retainPanels, mailboxId, threads],
662
+ [openThread, threads],
667
663
  );
668
664
 
669
665
  // Enter: open the focused row in the reading pane. This is the focus→open
@@ -30,7 +30,6 @@ import {
30
30
  useRovingFocus,
31
31
  } from "@remit/ui";
32
32
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
33
- import { useNavigate } from "@tanstack/react-router";
34
33
  import {
35
34
  AlertCircle,
36
35
  AlertTriangle,
@@ -62,7 +61,11 @@ import {
62
61
  import { isOutboxListRow, showsLastError } from "@/lib/outbox-status";
63
62
  import { normalizeSearchQuery } from "@/lib/search-query";
64
63
  import { parseSearchTokens } from "@/lib/search-tokens";
65
- import { useEditDraft, useRetainOpenPanels } from "@/routing";
64
+ import {
65
+ useCloseOutboxDraft,
66
+ useEditDraft,
67
+ useOpenOutboxDraft,
68
+ } from "@/routing";
66
69
 
67
70
  /* ------------------------------------------------------------------ */
68
71
  /* Helpers (shared between List, Reading, Phone sub-views) */
@@ -177,8 +180,8 @@ function OutboxPaneProvider({
177
180
  outboxMessageId: selectedMessageId,
178
181
  children,
179
182
  }: OutboxPaneProps) {
180
- const navigate = useNavigate();
181
- const retainPanels = useRetainOpenPanels();
183
+ const openDraft = useOpenOutboxDraft();
184
+ const closeDraft = useCloseOutboxDraft();
182
185
 
183
186
  const { data: outboxResponse, isLoading } = useQuery(
184
187
  outboxOperationsListOutboxMessagesOptions(),
@@ -218,26 +221,6 @@ function OutboxPaneProvider({
218
221
  [messages, selectedMessageId],
219
222
  );
220
223
 
221
- const handleOpenMessage = useCallback(
222
- (outboxMessageId: string) => {
223
- navigate({
224
- to: "/mail/outbox/draft/$outboxMessageId",
225
- params: { outboxMessageId },
226
- search: (prev) => prev,
227
- hash: retainPanels,
228
- });
229
- },
230
- [navigate, retainPanels],
231
- );
232
-
233
- const handleCloseMessage = useCallback(() => {
234
- navigate({
235
- to: "/mail/outbox",
236
- search: (prev) => prev,
237
- hash: retainPanels,
238
- });
239
- }, [navigate, retainPanels]);
240
-
241
224
  const ctx: OutboxPaneContextValue = {
242
225
  messages,
243
226
  hasQuery,
@@ -245,8 +228,8 @@ function OutboxPaneProvider({
245
228
  isLoading,
246
229
  selectedMessageId,
247
230
  selectedMessage,
248
- onOpenMessage: handleOpenMessage,
249
- onCloseMessage: handleCloseMessage,
231
+ onOpenMessage: openDraft,
232
+ onCloseMessage: closeDraft,
250
233
  };
251
234
 
252
235
  return (
@@ -70,8 +70,8 @@ import {
70
70
  } from "@/lib/organize/rule-model";
71
71
  import { searchRuleAccountId } from "@/lib/organize/search-to-rule";
72
72
  import type { OrganizeMatchPredicate } from "@/lib/organize/sender-fallback";
73
- import { useWizardEntryValue, useWizardStep } from "@/lib/wizard-history";
74
73
  import type { WizardSelectionMessage } from "@/lib/wizard-selection";
74
+ import { useWizardEntryValue, useWizardStep } from "@/routing";
75
75
  import { organizeRunState } from "./organize-run-state";
76
76
  import {
77
77
  retryIntent,
@@ -6,11 +6,10 @@ import {
6
6
  type SwipePeek,
7
7
  type ThreadRowData,
8
8
  } from "@remit/ui";
9
- import { useNavigate } from "@tanstack/react-router";
10
9
  import { useCallback, useState } from "react";
11
10
  import { toDisplayCategory } from "@/lib/display-category";
12
11
  import { formatEmailDate } from "@/lib/format";
13
- import { useRetainOpenPanels } from "@/routing";
12
+ import { useOpenThread } from "@/routing";
14
13
  import { MessageListItem } from "./MessageListItem";
15
14
  import { useModifierSelect } from "./useModifierSelect";
16
15
 
@@ -74,8 +73,7 @@ export const SwipeableMessageRow = ({
74
73
  density,
75
74
  }: SwipeableMessageRowProps) => {
76
75
  const [peek, setPeek] = useState<SwipePeek>("none");
77
- const navigate = useNavigate();
78
- const retainPanels = useRetainOpenPanels();
76
+ const openThread = useOpenThread();
79
77
 
80
78
  const handleAct = useCallback(
81
79
  (side: "leading" | "trailing") => {
@@ -99,17 +97,8 @@ export const SwipeableMessageRow = ({
99
97
  }, [onToggleCheck, thread.messageId]);
100
98
 
101
99
  const handleOpen = useCallback(() => {
102
- navigate({
103
- to: "/mail/$mailboxId/$threadId/$messageId",
104
- params: {
105
- mailboxId,
106
- threadId: thread.threadId,
107
- messageId: thread.messageId,
108
- },
109
- search: (prev) => prev,
110
- hash: retainPanels,
111
- });
112
- }, [navigate, retainPanels, mailboxId, thread.threadId, thread.messageId]);
100
+ openThread({ threadId: thread.threadId, messageId: thread.messageId });
101
+ }, [openThread, thread.threadId, thread.messageId]);
113
102
 
114
103
  const modifierSelect = useModifierSelect(thread.messageId, onRowSelect);
115
104
 
@@ -1,10 +1,10 @@
1
1
  import { mailboxOperationsListMailboxesOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
2
2
  import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
3
3
  import { useQueries } from "@tanstack/react-query";
4
- import { useLocation, useParams } from "@tanstack/react-router";
5
4
  import { useCallback } from "react";
6
5
  import { useTranslation } from "react-i18next";
7
6
  import { buildMailboxRoleMap, labelForMailbox } from "@/lib/folder-roles";
7
+ import { useBrowsedList } from "@/routing";
8
8
 
9
9
  interface UseCurrentMailboxNameOptions {
10
10
  accounts: RemitImapAccountResponse[];
@@ -27,9 +27,7 @@ interface UseCurrentMailboxNameOptions {
27
27
  export const useCurrentMailboxName = ({
28
28
  accounts,
29
29
  }: UseCurrentMailboxNameOptions): string | null => {
30
- const params = useParams({ strict: false });
31
- const location = useLocation();
32
- const mailboxId = (params as { mailboxId?: string }).mailboxId;
30
+ const { list, mailboxId } = useBrowsedList();
33
31
  const { t } = useTranslation("mail", { useSuspense: false });
34
32
  const translator = useCallback(
35
33
  (key: string, fallback: string) => t(key, { defaultValue: fallback }),
@@ -45,7 +43,7 @@ export const useCurrentMailboxName = ({
45
43
  })),
46
44
  });
47
45
 
48
- if (location.pathname.startsWith("/mail/outbox")) return "Outbox";
46
+ if (list === "outbox") return "Outbox";
49
47
  if (!mailboxId) return null;
50
48
 
51
49
  for (let i = 0; i < accounts.length; i++) {
@@ -74,8 +72,7 @@ export const useCurrentMailboxName = ({
74
72
  export const useCurrentMailboxUnseenCount = ({
75
73
  accounts,
76
74
  }: UseCurrentMailboxNameOptions): number | null => {
77
- const params = useParams({ strict: false });
78
- const mailboxId = (params as { mailboxId?: string }).mailboxId;
75
+ const { mailboxId } = useBrowsedList();
79
76
 
80
77
  const queries = useQueries({
81
78
  queries: accounts.map((account) => ({
@@ -107,8 +104,7 @@ export const useCurrentMailboxUnseenCount = ({
107
104
  export const useCurrentMailboxMessageCount = ({
108
105
  accounts,
109
106
  }: UseCurrentMailboxNameOptions): number => {
110
- const params = useParams({ strict: false });
111
- const mailboxId = (params as { mailboxId?: string }).mailboxId;
107
+ const { mailboxId } = useBrowsedList();
112
108
 
113
109
  const queries = useQueries({
114
110
  queries: accounts.map((account) => ({
@@ -1,5 +1,4 @@
1
1
  import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
2
- import { useNavigate, useRouterState } from "@tanstack/react-router";
3
2
  import { useCallback } from "react";
4
3
  import { useMailContext } from "@/lib/mail-context";
5
4
  import {
@@ -7,6 +6,7 @@ import {
7
6
  type SearchScopeState,
8
7
  searchScopeForRoute,
9
8
  } from "@/lib/search-scope";
9
+ import { useBrowsedList, useSearchEverything } from "@/routing";
10
10
  import { useCurrentMailboxName } from "./useCurrentMailboxName";
11
11
 
12
12
  /**
@@ -22,14 +22,11 @@ export function useSearchScope(accounts: RemitImapAccountResponse[]): {
22
22
  scope: SearchScopeState;
23
23
  clearScope: (chipId: string) => void;
24
24
  } {
25
- const navigate = useNavigate();
25
+ const searchEverything = useSearchEverything();
26
26
  const { searchInput } = useMailContext();
27
27
  const mailboxName = useCurrentMailboxName({ accounts });
28
- // Select the matches, not the scope: a select closing over `mailboxName`
29
- // only re-runs on router state changes, so the chip would keep reading the
30
- // previous folder until the next navigation.
31
- const matches = useRouterState({ select: (s) => s.matches });
32
- const scope = searchScopeForRoute(matches, mailboxName);
28
+ const browsed = useBrowsedList();
29
+ const scope = searchScopeForRoute(browsed, mailboxName);
33
30
 
34
31
  // Takes the chip id the field removed rather than assuming which chip that
35
32
  // was. The bar owns one chip today; keying on the id means a second one
@@ -37,12 +34,9 @@ export function useSearchScope(accounts: RemitImapAccountResponse[]): {
37
34
  const clearScope = useCallback(
38
35
  (chipId: string) => {
39
36
  if (chipId !== SEARCH_SCOPE_CHIP_ID) return;
40
- navigate({
41
- to: "/mail/brief",
42
- search: { q: searchInput || undefined },
43
- });
37
+ searchEverything(searchInput);
44
38
  },
45
- [navigate, searchInput],
39
+ [searchEverything, searchInput],
46
40
  );
47
41
 
48
42
  return { scope, clearScope };
@@ -1,7 +1,7 @@
1
- import { useRouterState } from "@tanstack/react-router";
2
1
  import { useMailContext } from "@/lib/mail-context";
3
2
  import { isScopedRoute } from "@/lib/search-scope";
4
3
  import type { SearchTokenContext } from "@/lib/search-tokens";
4
+ import { useBrowsedList } from "@/routing";
5
5
 
6
6
  /**
7
7
  * The one context every search engine parses the query through.
@@ -20,7 +20,8 @@ import type { SearchTokenContext } from "@/lib/search-tokens";
20
20
  */
21
21
  export function useSearchTokenContext(): SearchTokenContext {
22
22
  const { mailboxNameIndex, accountNameIndex } = useMailContext();
23
- const scoped = useRouterState({ select: (s) => isScopedRoute(s.matches) });
23
+ const browsed = useBrowsedList();
24
+ const scoped = isScopedRoute(browsed);
24
25
  return {
25
26
  ...(scoped ? {} : { mailboxesByName: mailboxNameIndex }),
26
27
  accountsByName: accountNameIndex,
@@ -2,11 +2,11 @@ import { semanticSearchOperationsSemanticSearchOptions } from "@remit/api-http-c
2
2
  import type { RemitImapSemanticSearchResult } from "@remit/api-http-client/types.gen.ts";
3
3
  import { MessageCategory } from "@remit/domain-enums";
4
4
  import { useQuery } from "@tanstack/react-query";
5
- import { useRouterState } from "@tanstack/react-router";
6
5
  import { useMailContext } from "@/lib/mail-context";
7
6
  import { normalizeSearchQuery } from "@/lib/search-query";
8
7
  import { semanticMailboxScope } from "@/lib/search-scope";
9
8
  import { parseSearchTokens, type SearchToken } from "@/lib/search-tokens";
9
+ import { useBrowsedList } from "@/routing";
10
10
  import { useSearchTokenContext } from "./useSearchTokenContext";
11
11
 
12
12
  /** Cap the "Related" section; the literal "Top matches" is the primary surface. */
@@ -79,7 +79,7 @@ export function useSemanticSearch({
79
79
  } {
80
80
  const { searchQuery } = useMailContext();
81
81
  const tokenContext = useSearchTokenContext();
82
- const matches = useRouterState({ select: (s) => s.matches });
82
+ const browsed = useBrowsedList();
83
83
  const normalizedQuery = normalizeSearchQuery(searchQuery);
84
84
  const { freeText, tokens } = parseSearchTokens(normalizedQuery, tokenContext);
85
85
  // `freeText`, not the raw query: a query of nothing but tokens
@@ -105,7 +105,7 @@ export function useSemanticSearch({
105
105
  const inToken = tokens.find((t) => t.type === "in");
106
106
  // The route decides the scope, not the call site — see `semanticMailboxScope`.
107
107
  const effectiveMailboxId = semanticMailboxScope({
108
- matches,
108
+ browsed,
109
109
  callerMailboxId: mailboxId,
110
110
  inTokenMailboxId: inToken?.mailboxId,
111
111
  });
@@ -30,6 +30,16 @@ export type MailListRoute =
30
30
  | { list: "outbox" }
31
31
  | { list: "mailbox"; mailboxId: string | undefined };
32
32
 
33
+ /**
34
+ * The list a caller is on, flattened to the two values a navigation to it — or
35
+ * a decision about it — needs. Both are primitives, so a caller holding them in
36
+ * a dependency array settles, and `list` is `undefined` outside the mail shell.
37
+ */
38
+ export interface BrowsedList {
39
+ list: MailListRoute["list"] | undefined;
40
+ mailboxId: string | undefined;
41
+ }
42
+
33
43
  /**
34
44
  * The list layout among the matched routes, or `undefined` outside the mail
35
45
  * shell.