@remit/web-client 0.0.201 → 0.0.203

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 (50) hide show
  1. package/package.json +1 -1
  2. package/src/components/compose/ComposeSmtpMissingBanner.tsx +3 -6
  3. package/src/components/layout/ComposeFab.tsx +3 -5
  4. package/src/components/layout/MailTopBar.tsx +3 -4
  5. package/src/components/mail/BriefPane.tsx +14 -31
  6. package/src/components/mail/DailyBrief.tsx +9 -12
  7. package/src/components/mail/DraftsView.tsx +3 -17
  8. package/src/components/mail/FlaggedList.tsx +1 -1
  9. package/src/components/mail/FlaggedPane.tsx +14 -31
  10. package/src/components/mail/MailListHeader.tsx +10 -11
  11. package/src/components/mail/MailSidebarAdapter.tsx +4 -25
  12. package/src/components/mail/MailboxPane.tsx +21 -59
  13. package/src/components/mail/MessageActionMenu.tsx +4 -11
  14. package/src/components/mail/MessageList.tsx +9 -13
  15. package/src/components/mail/OutboxPane.tsx +9 -26
  16. package/src/components/mail/SelectionWizardHost.tsx +1 -1
  17. package/src/components/mail/SwipeableMessageRow.tsx +4 -15
  18. package/src/hooks/useCurrentMailboxName.ts +5 -9
  19. package/src/hooks/useSearchScope.ts +6 -12
  20. package/src/hooks/useSearchTokenContext.ts +3 -2
  21. package/src/hooks/useSemanticSearch.ts +3 -3
  22. package/src/lib/delete-folder.test.ts +17 -0
  23. package/src/lib/delete-folder.ts +3 -1
  24. package/src/lib/mail-route.ts +10 -0
  25. package/src/lib/new-folder.test.ts +18 -0
  26. package/src/lib/new-folder.ts +4 -3
  27. package/src/lib/search-scope.test.ts +45 -32
  28. package/src/lib/search-scope.ts +14 -19
  29. package/src/lib/search-view.test.ts +1 -1
  30. package/src/lib/search-view.ts +2 -2
  31. package/src/routes/mail/$mailboxId.tsx +1 -2
  32. package/src/routes/mail/brief.tsx +1 -2
  33. package/src/routes/mail/flagged.tsx +1 -2
  34. package/src/routes/mail/outbox.tsx +1 -2
  35. package/src/routes/mail.tsx +7 -3
  36. package/src/routing/browsed-list.ts +60 -9
  37. package/src/routing/compose.ts +2 -27
  38. package/src/routing/index.ts +40 -1
  39. package/src/routing/nav-selection.ts +24 -0
  40. package/src/routing/open-thread.ts +89 -2
  41. package/src/routing/outbox-draft.ts +27 -1
  42. package/src/{hooks/useSearchField.render.test.ts → routing/search-field.render.test.ts} +1 -1
  43. package/src/{hooks/useSearchField.ts → routing/search-field.ts} +1 -1
  44. package/src/{hooks → routing}/search-mirror-convergence.render.test.ts +1 -1
  45. package/src/{hooks → routing}/search-mirror-detail.render.test.ts +1 -1
  46. package/src/{hooks/useSearchMirror.ts → routing/search-mirror.ts} +2 -1
  47. package/src/routing/search-navigation.ts +44 -0
  48. package/src/routing/sections.ts +53 -0
  49. /package/src/{lib → routing}/wizard-history.test.ts +0 -0
  50. /package/src/{lib → routing}/wizard-history.ts +0 -0
@@ -1,5 +1,7 @@
1
- import { useParams } from "@tanstack/react-router";
2
- import { useMemo } from "react";
1
+ import { useNavigate, useParams } from "@tanstack/react-router";
2
+ import { useCallback, useMemo } from "react";
3
+ import { useBrowsedList, useNavigateToBrowsedList } from "./browsed-list";
4
+ import { useRetainOpenPanels } from "./fragment";
3
5
 
4
6
  /** The conversation a list has open, as the address states it. */
5
7
  export interface OpenThreadPath {
@@ -68,3 +70,88 @@ export function useOpenThreadPath(): OpenThreadPath | undefined {
68
70
  [threadId, messageId],
69
71
  );
70
72
  }
73
+
74
+ /** How an open differs from the plain one, where it does. */
75
+ export interface OpenThreadOptions {
76
+ /** Replace the entry rather than push one, so Back skips the row. */
77
+ replace?: boolean;
78
+ /**
79
+ * Commit this query with the open. The debounced q-mirror walks back up to
80
+ * the list when a query goes active, so a row tapped before the debounce
81
+ * settles is closed again a moment later unless the address already says the
82
+ * query the conversation was opened under. Omitted, the open carries whatever
83
+ * query the address already had.
84
+ */
85
+ query?: string;
86
+ /**
87
+ * Open under this mailbox rather than the one being browsed. A search hit
88
+ * carries its own, so a row can never open under a mailbox it does not belong
89
+ * to.
90
+ */
91
+ mailboxId?: string;
92
+ }
93
+
94
+ /**
95
+ * Open a conversation on the list the reader is browsing.
96
+ *
97
+ * The thread is a child route of its list, so the list decides the address and
98
+ * the caller only names the conversation. The brief is the fallback, and legal
99
+ * for any thread: a list with no thread route under it — the outbox — and a
100
+ * mailbox route in the frame before its id resolves both still open the
101
+ * conversation rather than doing nothing, which is the dead row this route shape
102
+ * exists to delete.
103
+ */
104
+ export function useOpenThread(): (
105
+ target: OpenThreadTarget,
106
+ options?: OpenThreadOptions,
107
+ ) => void {
108
+ const navigate = useNavigate();
109
+ const retainPanels = useRetainOpenPanels();
110
+ const { list, mailboxId: browsedMailboxId } = useBrowsedList();
111
+
112
+ return useCallback(
113
+ (target: OpenThreadTarget, options?: OpenThreadOptions) => {
114
+ const query = options?.query;
115
+ const search = (prev: Record<string, unknown>) =>
116
+ query === undefined ? prev : { ...prev, q: query || undefined };
117
+ const rest = {
118
+ search,
119
+ hash: retainPanels,
120
+ replace: options?.replace ?? false,
121
+ };
122
+ if (list === "flagged") {
123
+ navigate({
124
+ to: "/mail/flagged/$threadId/$messageId",
125
+ params: target,
126
+ ...rest,
127
+ });
128
+ return;
129
+ }
130
+ const mailboxId = options?.mailboxId ?? browsedMailboxId;
131
+ if (list === "mailbox" && mailboxId) {
132
+ navigate({
133
+ to: "/mail/$mailboxId/$threadId/$messageId",
134
+ params: { mailboxId, ...target },
135
+ ...rest,
136
+ });
137
+ return;
138
+ }
139
+ navigate({
140
+ to: "/mail/brief/$threadId/$messageId",
141
+ params: target,
142
+ ...rest,
143
+ });
144
+ },
145
+ [navigate, retainPanels, list, browsedMailboxId],
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Close the conversation, landing back on the list it was opened from.
151
+ *
152
+ * A navigation up rather than a flag, so nothing is left mounted below the list
153
+ * and Esc unwinds exactly one step.
154
+ */
155
+ export function useCloseThread(): () => void {
156
+ return useNavigateToBrowsedList();
157
+ }
@@ -1,4 +1,7 @@
1
- import { useParams } from "@tanstack/react-router";
1
+ import { useNavigate, useParams } from "@tanstack/react-router";
2
+ import { useCallback } from "react";
3
+ import { useNavigateToBrowsedList } from "./browsed-list";
4
+ import { useRetainOpenPanels } from "./fragment";
2
5
 
3
6
  /**
4
7
  * The outbox message the reader has open, read off the path.
@@ -15,3 +18,26 @@ export function useOutboxDraftId(): string | undefined {
15
18
  });
16
19
  return draft?.outboxMessageId;
17
20
  }
21
+
22
+ /** Open an outbox message in the reading pane. */
23
+ export function useOpenOutboxDraft(): (outboxMessageId: string) => void {
24
+ const navigate = useNavigate();
25
+ const retainPanels = useRetainOpenPanels();
26
+
27
+ return useCallback(
28
+ (outboxMessageId: string) => {
29
+ navigate({
30
+ to: "/mail/outbox/draft/$outboxMessageId",
31
+ params: { outboxMessageId },
32
+ search: (prev: Record<string, unknown>) => prev,
33
+ hash: retainPanels,
34
+ });
35
+ },
36
+ [navigate, retainPanels],
37
+ );
38
+ }
39
+
40
+ /** Close it again, landing back on the outbox. */
41
+ export function useCloseOutboxDraft(): () => void {
42
+ return useNavigateToBrowsedList();
43
+ }
@@ -34,7 +34,7 @@ import {
34
34
  } from "@tanstack/react-router";
35
35
  import { createElement, useState } from "react";
36
36
  import { createDomHarness, type DomHarness } from "../test-support/dom";
37
- import { useSearchField } from "./useSearchField";
37
+ import { useSearchField } from "./search-field";
38
38
 
39
39
  let harness: DomHarness | undefined;
40
40
 
@@ -1,8 +1,8 @@
1
1
  import { useRouter, useRouterState } from "@tanstack/react-router";
2
2
  import { useCallback, useState } from "react";
3
+ import { useDebouncedValue } from "@/hooks/useDebouncedValue";
3
4
  import { addressQuery, mailViewKey } from "@/lib/mail-route";
4
5
  import { committedSearchQuery, searchInputForView } from "@/lib/search-view";
5
- import { useDebouncedValue } from "./useDebouncedValue";
6
6
 
7
7
  export interface SearchField {
8
8
  /** The live field text, which every search surface binds. */
@@ -34,7 +34,7 @@ import { createElement, useState } from "react";
34
34
  import { MailContext, type MailContextValue } from "@/lib/mail-context";
35
35
  import { EMPTY_RESULT_FOLDER_INDEX } from "@/lib/result-folder";
36
36
  import { createDomHarness, type DomHarness } from "../test-support/dom";
37
- import { useSearchMirror } from "./useSearchMirror";
37
+ import { useSearchMirror } from "./search-mirror";
38
38
 
39
39
  let harness: DomHarness | undefined;
40
40
 
@@ -32,7 +32,7 @@ import { MailContext, type MailContextValue } from "@/lib/mail-context";
32
32
  import { mailViewKey } from "@/lib/mail-route";
33
33
  import { EMPTY_RESULT_FOLDER_INDEX } from "@/lib/result-folder";
34
34
  import { createDomHarness, type DomHarness } from "../test-support/dom";
35
- import { useSearchMirror } from "./useSearchMirror";
35
+ import { useSearchMirror } from "./search-mirror";
36
36
 
37
37
  let harness: DomHarness | undefined;
38
38
 
@@ -3,7 +3,8 @@ import { useEffect } from "react";
3
3
  import { useMailContext } from "@/lib/mail-context";
4
4
  import { addressQuery } from "@/lib/mail-route";
5
5
  import { shouldMirrorQuery } from "@/lib/search-view";
6
- import { useIsComposing, useIsReplying } from "@/routing";
6
+ import { useIsComposing } from "./compose";
7
+ import { useIsReplying } from "./reply";
7
8
 
8
9
  /**
9
10
  * The list the mirror writes to. Each list calls the hook with its own route,
@@ -0,0 +1,44 @@
1
+ import { useNavigate } from "@tanstack/react-router";
2
+ import { useCallback } from "react";
3
+
4
+ /**
5
+ * Search everything, carrying the query.
6
+ *
7
+ * "Everything" is the daily brief — the cross-account view whose scope is
8
+ * nothing — so dropping the scope chip and running a saved search are the same
9
+ * movement, and both are a navigation rather than an edit of the query text:
10
+ * the chip mirrors the route (see `lib/search-scope.ts`), so editing the text
11
+ * would leave the chip up and the list still narrowed.
12
+ */
13
+ export function useSearchEverything(): (query: string) => void {
14
+ const navigate = useNavigate();
15
+ return useCallback(
16
+ (query: string) => {
17
+ navigate({ to: "/mail/brief", search: { q: query || undefined } });
18
+ },
19
+ [navigate],
20
+ );
21
+ }
22
+
23
+ /**
24
+ * Narrow the search to one mailbox, carrying the query.
25
+ *
26
+ * The route is the scope, so the offer to look in Spam for what a global search
27
+ * held back goes to Spam rather than rewriting the query with an `in:` term.
28
+ */
29
+ export function useScopeSearchToMailbox(): (
30
+ mailboxId: string,
31
+ query: string,
32
+ ) => void {
33
+ const navigate = useNavigate();
34
+ return useCallback(
35
+ (mailboxId: string, query: string) => {
36
+ navigate({
37
+ to: "/mail/$mailboxId",
38
+ params: { mailboxId },
39
+ search: { q: query || undefined },
40
+ });
41
+ },
42
+ [navigate],
43
+ );
44
+ }
@@ -0,0 +1,53 @@
1
+ import { useNavigate } from "@tanstack/react-router";
2
+ import { useCallback } from "react";
3
+
4
+ /**
5
+ * A top-level destination, as the sidebar and the keyboard name it. These are
6
+ * the moves that leave whatever the reader is on rather than open something
7
+ * under it, so they carry no panels and no query.
8
+ */
9
+ export type AppSection =
10
+ | "brief"
11
+ | "flagged"
12
+ | "outbox"
13
+ | "settings"
14
+ | "accounts";
15
+
16
+ const sectionRoute = {
17
+ brief: "/mail/brief",
18
+ flagged: "/mail/flagged",
19
+ outbox: "/mail/outbox",
20
+ settings: "/settings",
21
+ accounts: "/settings/accounts",
22
+ } as const satisfies Record<AppSection, string>;
23
+
24
+ /** Go to a top-level section by name, so no caller writes a path. */
25
+ export function useGoToSection(): (section: AppSection) => void {
26
+ const navigate = useNavigate();
27
+ return useCallback(
28
+ (section: AppSection) => {
29
+ navigate({ to: sectionRoute[section] });
30
+ },
31
+ [navigate],
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Open one account's settings on its outgoing-mail fields.
37
+ *
38
+ * The account and the field to land on ride the address, so the banner that
39
+ * sends the reader there is not also responsible for telling the settings pane
40
+ * what it was about.
41
+ */
42
+ export function useConfigureAccountSmtp(): (accountId: string) => void {
43
+ const navigate = useNavigate();
44
+ return useCallback(
45
+ (accountId: string) => {
46
+ navigate({
47
+ to: "/settings/accounts",
48
+ search: { editAccountId: accountId, focusSmtp: true },
49
+ });
50
+ },
51
+ [navigate],
52
+ );
53
+ }
File without changes
File without changes