@remit/web-client 0.0.149 → 0.0.151

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/components/compose/ComposeProvider.tsx +27 -5
  3. package/src/components/layout/ComposeFab.tsx +3 -2
  4. package/src/components/mail/BriefPane.tsx +8 -8
  5. package/src/components/mail/DailyBrief.tsx +3 -7
  6. package/src/components/mail/DraftsView.tsx +7 -3
  7. package/src/components/mail/IntelligencePane.tsx +4 -3
  8. package/src/components/mail/MailListHeader.tsx +1 -5
  9. package/src/components/mail/MailSidebarAdapter.tsx +4 -3
  10. package/src/components/mail/MailboxPane.tsx +130 -150
  11. package/src/components/mail/MessageActionMenu.tsx +12 -8
  12. package/src/components/mail/MessageList.selection.test.ts +6 -7
  13. package/src/components/mail/MessageList.tsx +28 -32
  14. package/src/components/mail/MessageListItem.tsx +2 -1
  15. package/src/components/mail/MessageRow.tsx +13 -10
  16. package/src/components/mail/OutboxPane.tsx +31 -28
  17. package/src/components/mail/SwipeableMessageRow.modifier-select.test.ts +8 -3
  18. package/src/components/mail/SwipeableMessageRow.tsx +8 -12
  19. package/src/hooks/useIntelligenceData.ts +1 -0
  20. package/src/lib/conversation-target.ts +7 -49
  21. package/src/lib/mail-search.ts +13 -11
  22. package/src/routeTree.gen.ts +96 -0
  23. package/src/routes/mail/$mailboxId/$threadId/$messageId.tsx +19 -0
  24. package/src/routes/mail/$mailboxId/$threadId/index.tsx +14 -0
  25. package/src/routes/mail/$mailboxId/$threadId.tsx +12 -0
  26. package/src/routes/mail/$mailboxId.tsx +7 -3
  27. package/src/routes/mail/brief.tsx +2 -2
  28. package/src/routes/mail/outbox/draft/$outboxMessageId.tsx +19 -0
  29. package/src/routes/mail/outbox.tsx +6 -1
  30. package/src/routing/index.ts +6 -5
  31. package/src/routing/open-thread.ts +58 -0
  32. package/src/routing/outbox-draft.ts +17 -0
  33. package/src/lib/conversation-target.test.ts +0 -68
  34. package/src/lib/search-pending.test.ts +0 -99
  35. package/src/lib/search-pending.ts +0 -49
  36. package/src/routing/brief-thread.ts +0 -47
@@ -4,9 +4,9 @@
4
4
  * The mailbox list, the daily brief and Flagged used to carry three
5
5
  * near-identical rows (#149). They differ in two axes only, both props here:
6
6
  *
7
- * - `linkMailboxId` — set for the mailbox list, whose rows are real links so a
8
- * plain click routes and a middle click opens a tab. Omitted for the brief
9
- * and Flagged, whose rows report the tap through `onClick`.
7
+ * - `link` — set for the mailbox list, whose rows are real links so a plain
8
+ * click routes and a middle click opens a tab. Omitted for the brief and
9
+ * Flagged, whose rows report the tap through `onClick`.
10
10
  * - `selection` — set where multi-select is available. Absent renders the
11
11
  * avatar alone with no checkbox, which is what "non-selectable mode" means.
12
12
  */
@@ -73,8 +73,11 @@ export interface MessageRowProps {
73
73
  * orphan `option` is invalid ARIA and costs the row its button semantics.
74
74
  */
75
75
  inListbox?: boolean;
76
- /** Mailbox whose route the row links to; omit for callback-driven rows. */
77
- linkMailboxId?: string;
76
+ /**
77
+ * The address the row links to: the folder being browsed and the conversation
78
+ * this row opens. Omit for callback-driven rows.
79
+ */
80
+ link?: { mailboxId: string; threadId: string };
78
81
  /** Called when the row is opened by a plain click on a non-linking row. */
79
82
  onClick?: () => void;
80
83
  /** Called when the row takes DOM focus, so the roving cursor follows it. */
@@ -91,7 +94,7 @@ const MessageRowComponent = ({
91
94
  badge,
92
95
  selection: selectionProp,
93
96
  inListbox = false,
94
- linkMailboxId,
97
+ link,
95
98
  onClick,
96
99
  onFocusRow: onFocusRowProp,
97
100
  }: MessageRowProps) => {
@@ -240,7 +243,7 @@ const MessageRowComponent = ({
240
243
  />
241
244
  );
242
245
 
243
- if (linkMailboxId === undefined) {
246
+ if (link === undefined) {
244
247
  return (
245
248
  <button type="button" {...interactionProps} className={className}>
246
249
  {body}
@@ -250,9 +253,9 @@ const MessageRowComponent = ({
250
253
 
251
254
  return (
252
255
  <NavLink
253
- to="/mail/$mailboxId"
254
- params={{ mailboxId: linkMailboxId }}
255
- search={(prev) => ({ ...prev, selectedMessageId: messageId })}
256
+ to="/mail/$mailboxId/$threadId/$messageId"
257
+ params={{ ...link, messageId }}
258
+ search={(prev) => prev}
256
259
  variant="row"
257
260
  {...interactionProps}
258
261
  className={className}
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * OutboxPane — compound component for the outbox view (/mail/outbox route).
3
3
  *
4
- * Usage in mail.tsx:
4
+ * Usage in the list layout route:
5
5
  *
6
- * <OutboxPane>
7
- * <AppShellSlotted
6
+ * <OutboxPane outboxMessageId={useOutboxDraftId()}>
7
+ * <MailShell
8
8
  * list={<OutboxPane.List />}
9
- * reading={<OutboxPane.Reading />}
9
+ * reading={<Outlet />}
10
10
  * />
11
11
  * </OutboxPane>
12
12
  *
@@ -29,7 +29,7 @@ import {
29
29
  useRovingFocus,
30
30
  } from "@remit/ui";
31
31
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
32
- import { useNavigate, useSearch } from "@tanstack/react-router";
32
+ import { useNavigate } from "@tanstack/react-router";
33
33
  import {
34
34
  AlertCircle,
35
35
  AlertTriangle,
@@ -143,9 +143,11 @@ interface OutboxPaneContextValue {
143
143
  /** True when that query asks for a filter the outbox cannot apply. */
144
144
  unsupportedQuery: boolean;
145
145
  isLoading: boolean;
146
+ /** The message the address names, which is the one the list highlights. */
146
147
  selectedMessageId: string | undefined;
147
148
  selectedMessage: RemitImapOutboxMessageResponse | undefined;
148
- onSelectMessage: (id: string | undefined) => void;
149
+ onOpenMessage: (outboxMessageId: string) => void;
150
+ onCloseMessage: () => void;
149
151
  }
150
152
 
151
153
  const OutboxPaneCtx = createContext<OutboxPaneContextValue | null>(null);
@@ -161,16 +163,16 @@ function useOutboxPane(): OutboxPaneContextValue {
161
163
  /* ------------------------------------------------------------------ */
162
164
 
163
165
  interface OutboxPaneProps {
166
+ /** The open message, as the address states it. */
167
+ outboxMessageId: string | undefined;
164
168
  children: ReactNode;
165
169
  }
166
170
 
167
- function OutboxPaneProvider({ children }: OutboxPaneProps) {
171
+ function OutboxPaneProvider({
172
+ outboxMessageId: selectedMessageId,
173
+ children,
174
+ }: OutboxPaneProps) {
168
175
  const navigate = useNavigate();
169
- // Read selectedOutboxMessageId from the URL directly — this avoids threading
170
- // a prop through the parent shell for a param that only outbox uses.
171
- const { selectedOutboxMessageId: selectedMessageId } = useSearch({
172
- strict: false,
173
- }) as { selectedOutboxMessageId?: string };
174
176
 
175
177
  const { data: outboxResponse, isLoading } = useQuery(
176
178
  outboxOperationsListOutboxMessagesOptions(),
@@ -210,19 +212,21 @@ function OutboxPaneProvider({ children }: OutboxPaneProps) {
210
212
  [messages, selectedMessageId],
211
213
  );
212
214
 
213
- const handleSelectMessage = useCallback(
214
- (id: string | undefined) => {
215
+ const handleOpenMessage = useCallback(
216
+ (outboxMessageId: string) => {
215
217
  navigate({
216
- to: "/mail/outbox",
217
- search: (prev) => ({
218
- ...prev,
219
- selectedOutboxMessageId: id,
220
- }),
218
+ to: "/mail/outbox/draft/$outboxMessageId",
219
+ params: { outboxMessageId },
220
+ search: (prev) => prev,
221
221
  });
222
222
  },
223
223
  [navigate],
224
224
  );
225
225
 
226
+ const handleCloseMessage = useCallback(() => {
227
+ navigate({ to: "/mail/outbox", search: (prev) => prev });
228
+ }, [navigate]);
229
+
226
230
  const ctx: OutboxPaneContextValue = {
227
231
  messages,
228
232
  hasQuery,
@@ -230,7 +234,8 @@ function OutboxPaneProvider({ children }: OutboxPaneProps) {
230
234
  isLoading,
231
235
  selectedMessageId,
232
236
  selectedMessage,
233
- onSelectMessage: handleSelectMessage,
237
+ onOpenMessage: handleOpenMessage,
238
+ onCloseMessage: handleCloseMessage,
234
239
  };
235
240
 
236
241
  return (
@@ -438,7 +443,7 @@ function OutboxList() {
438
443
  unsupportedQuery,
439
444
  isLoading,
440
445
  selectedMessageId,
441
- onSelectMessage,
446
+ onOpenMessage,
442
447
  } = useOutboxPane();
443
448
  const listRef = useRef<HTMLDivElement>(null);
444
449
  useRovingFocus({ containerRef: listRef, itemSelector: LIST_ROW_SELECTOR });
@@ -482,7 +487,7 @@ function OutboxList() {
482
487
  key={message.outboxMessageId}
483
488
  message={message}
484
489
  isSelected={selectedMessageId === message.outboxMessageId}
485
- onSelect={() => onSelectMessage(message.outboxMessageId)}
490
+ onSelect={() => onOpenMessage(message.outboxMessageId)}
486
491
  />
487
492
  ))}
488
493
  </div>
@@ -529,7 +534,8 @@ function OutboxPhone() {
529
534
  isLoading,
530
535
  selectedMessageId,
531
536
  selectedMessage,
532
- onSelectMessage,
537
+ onOpenMessage,
538
+ onCloseMessage,
533
539
  } = useOutboxPane();
534
540
  const listRef = useRef<HTMLDivElement>(null);
535
541
  useRovingFocus({ containerRef: listRef, itemSelector: LIST_ROW_SELECTOR });
@@ -544,10 +550,7 @@ function OutboxPhone() {
544
550
 
545
551
  if (selectedMessage) {
546
552
  return (
547
- <OutboxMessageDetail
548
- message={selectedMessage}
549
- onBack={() => onSelectMessage(undefined)}
550
- />
553
+ <OutboxMessageDetail message={selectedMessage} onBack={onCloseMessage} />
551
554
  );
552
555
  }
553
556
 
@@ -579,7 +582,7 @@ function OutboxPhone() {
579
582
  key={message.outboxMessageId}
580
583
  message={message}
581
584
  isSelected={selectedMessageId === message.outboxMessageId}
582
- onSelect={() => onSelectMessage(message.outboxMessageId)}
585
+ onSelect={() => onOpenMessage(message.outboxMessageId)}
583
586
  />
584
587
  ))}
585
588
  </div>
@@ -64,6 +64,10 @@ const mailRoute = createRoute({
64
64
  getParentRoute: () => rootRoute,
65
65
  path: "/mail/$mailboxId",
66
66
  });
67
+ const messageRoute = createRoute({
68
+ getParentRoute: () => mailRoute,
69
+ path: "$threadId/$messageId",
70
+ });
67
71
 
68
72
  interface Mounted {
69
73
  row: HTMLElement;
@@ -74,7 +78,7 @@ interface Mounted {
74
78
  const mountRow = (selectionTakesIt = true): Mounted => {
75
79
  const selects: SelectionModifiers[] = [];
76
80
  const router = createRouter({
77
- routeTree: rootRoute.addChildren([mailRoute]),
81
+ routeTree: rootRoute.addChildren([mailRoute.addChildren([messageRoute])]),
78
82
  history: createMemoryHistory({ initialEntries: ["/mail/mbx-1"] }),
79
83
  }) as unknown as AnyRouter;
80
84
  const created = createDomHarness({ viewportWidth: HALF_SCREEN_WIDTH });
@@ -148,9 +152,10 @@ const click = (
148
152
  }),
149
153
  );
150
154
 
155
+ // The conversation the row opened is the address, so the message it named is the
156
+ // last segment of the path.
151
157
  const openedMessageId = (router: AnyRouter): string | undefined =>
152
- (router.state.location.search as { selectedMessageId?: string })
153
- .selectedMessageId;
158
+ router.state.location.pathname.split("/")[4];
154
159
 
155
160
  describe("SwipeableMessageRow — modifier selection below the desktop width", () => {
156
161
  it("takes a shift-press for selection instead of opening the message", async () => {
@@ -13,11 +13,6 @@ import { formatEmailDate } from "@/lib/format";
13
13
  import { MessageListItem } from "./MessageListItem";
14
14
  import { useModifierSelect } from "./useModifierSelect";
15
15
 
16
- interface MailboxLinkSearch {
17
- selectedMessageId?: string;
18
- q?: string;
19
- }
20
-
21
16
  interface SwipeableMessageRowProps {
22
17
  thread: RemitImapThreadMessageResponse;
23
18
  mailboxId: string;
@@ -103,14 +98,15 @@ export const SwipeableMessageRow = ({
103
98
 
104
99
  const handleOpen = useCallback(() => {
105
100
  navigate({
106
- to: "/mail/$mailboxId",
107
- params: { mailboxId },
108
- search: (prev: MailboxLinkSearch) => ({
109
- ...prev,
110
- selectedMessageId: thread.messageId,
111
- }),
101
+ to: "/mail/$mailboxId/$threadId/$messageId",
102
+ params: {
103
+ mailboxId,
104
+ threadId: thread.threadId,
105
+ messageId: thread.messageId,
106
+ },
107
+ search: (prev) => prev,
112
108
  });
113
- }, [navigate, mailboxId, thread.messageId]);
109
+ }, [navigate, mailboxId, thread.threadId, thread.messageId]);
114
110
 
115
111
  const modifierSelect = useModifierSelect(thread.messageId, onRowSelect);
116
112
 
@@ -363,6 +363,7 @@ export function useIntelligenceData(
363
363
  .filter((r) => r.messageId !== thread.messageId)
364
364
  .map((r) => ({
365
365
  id: r.messageId,
366
+ threadId: r.threadId,
366
367
  // Prefer the mailbox the user is already viewing when the message also
367
368
  // lives there, so opening a similar message stays in the same folder;
368
369
  // otherwise fall back to the first mailbox it lives in.
@@ -1,14 +1,12 @@
1
1
  /**
2
- * The minimal data needed to open a conversation. A list view normally opens the
3
- * thread it already has loaded, but a semantic "Related" search hit can point at a
4
- * message that isn't in the loaded list at all. Such a hit still carries its
5
- * `threadId` and `mailboxId`, and `ConversationView` fetches by `threadId`, so we
6
- * can open it directly from those alone no dependency on the loaded list.
2
+ * What the reading pane needs to show a conversation.
3
+ *
4
+ * The thread is the whole address `ConversationView` fetches by it and the
5
+ * rest is what the list already knows about the row the reader pointed at, so
6
+ * the pane paints a subject before the thread's own messages arrive. A thread no
7
+ * list holds carries none of it and still opens.
7
8
  */
8
- import type {
9
- RemitImapMessageAuthenticity,
10
- RemitImapThreadMessageResponse,
11
- } from "@remit/api-http-client/types.gen.ts";
9
+ import type { RemitImapMessageAuthenticity } from "@remit/api-http-client/types.gen.ts";
12
10
 
13
11
  export interface ConversationTarget {
14
12
  threadId: string;
@@ -17,43 +15,3 @@ export interface ConversationTarget {
17
15
  messageId?: string;
18
16
  authenticity?: RemitImapMessageAuthenticity;
19
17
  }
20
-
21
- function threadToConversationTarget(
22
- thread: RemitImapThreadMessageResponse,
23
- ): ConversationTarget {
24
- return {
25
- threadId: thread.threadId,
26
- mailboxId: thread.mailboxId,
27
- subject: thread.subject,
28
- messageId: thread.messageId,
29
- authenticity: thread.authenticity,
30
- };
31
- }
32
-
33
- /**
34
- * Resolve the conversation to open. Prefer the fully loaded thread (it carries
35
- * the subject, authenticity and read state the reading pane wants); otherwise
36
- * fall back to the `threadId` + `mailboxId` carried in the URL by a tapped
37
- * "Related" hit so a message outside the loaded list still opens.
38
- */
39
- export function buildConversationTarget(
40
- selectedThread: RemitImapThreadMessageResponse | undefined,
41
- fallback: {
42
- messageId?: string;
43
- threadId?: string;
44
- mailboxId?: string;
45
- },
46
- ): ConversationTarget | undefined {
47
- if (selectedThread) return threadToConversationTarget(selectedThread);
48
- // Require the messageId too: it's always set alongside the threadId when a
49
- // result is tapped, and gating on it means clearing `selectedMessageId` (e.g.
50
- // pressing Back) closes the conversation even if a stale threadId lingers.
51
- if (fallback.messageId && fallback.threadId && fallback.mailboxId) {
52
- return {
53
- threadId: fallback.threadId,
54
- mailboxId: fallback.mailboxId,
55
- messageId: fallback.messageId,
56
- };
57
- }
58
- return undefined;
59
- }
@@ -16,7 +16,7 @@ const listSearch = z.object({ q: z.string().optional() });
16
16
  * The brief's open thread and the message inside it are path segments
17
17
  * (`/mail/brief/<thread>/<message>`), so the query carries nothing but the
18
18
  * search. An old link's selection params are dropped here, which is what
19
- * "tolerated and ignored" means until the other three lists follow.
19
+ * "tolerated and ignored" means until the remaining lists follow.
20
20
  */
21
21
  export const briefSearchSchema = listSearch.extend({});
22
22
 
@@ -24,17 +24,19 @@ export const flaggedSearchSchema = listSearch.extend({
24
24
  selectedMessageId: z.string().optional(),
25
25
  });
26
26
 
27
- export const outboxSearchSchema = listSearch.extend({
28
- selectedOutboxMessageId: z.string().optional(),
29
- });
27
+ /**
28
+ * The outbox's open message is a path segment
29
+ * (`/mail/outbox/draft/<outboxMessageId>`), so the query carries nothing but
30
+ * the search. An old link's selection param is dropped here.
31
+ */
32
+ export const outboxSearchSchema = listSearch.extend({});
30
33
 
31
- export const mailboxSearchSchema = listSearch.extend({
32
- selectedMessageId: z.string().optional(),
33
- // A tapped semantic "Related" hit can point at a message outside the loaded
34
- // list; carrying its thread lets the mailbox open it directly (the mailbox is
35
- // the route param). See `buildConversationTarget`.
36
- selectedThreadId: z.string().optional(),
37
- });
34
+ /**
35
+ * A folder's open thread and the message inside it are path segments
36
+ * (`/mail/<mailbox>/<thread>/<message>`), so the query carries nothing but the
37
+ * search.
38
+ */
39
+ export const mailboxSearchSchema = listSearch.extend({});
38
40
 
39
41
  /**
40
42
  * `/mail` itself only redirects to the brief, so it accepts what the brief
@@ -28,12 +28,16 @@ import { Route as SettingsLabelsRouteImport } from './routes/settings/labels'
28
28
  import { Route as SettingsSendersRouteImport } from './routes/settings/senders'
29
29
  import { Route as SettingsSuggestedVipsRouteImport } from './routes/settings/suggested-vips'
30
30
  import { Route as MailMailboxIdIndexRouteImport } from './routes/mail/$mailboxId/index'
31
+ import { Route as MailMailboxIdThreadIdRouteImport } from './routes/mail/$mailboxId/$threadId'
31
32
  import { Route as MailBriefIndexRouteImport } from './routes/mail/brief/index'
32
33
  import { Route as MailBriefThreadIdRouteImport } from './routes/mail/brief/$threadId'
33
34
  import { Route as MailFlaggedIndexRouteImport } from './routes/mail/flagged/index'
34
35
  import { Route as MailOutboxIndexRouteImport } from './routes/mail/outbox/index'
36
+ import { Route as MailMailboxIdThreadIdIndexRouteImport } from './routes/mail/$mailboxId/$threadId/index'
37
+ import { Route as MailMailboxIdThreadIdMessageIdRouteImport } from './routes/mail/$mailboxId/$threadId/$messageId'
35
38
  import { Route as MailBriefThreadIdIndexRouteImport } from './routes/mail/brief/$threadId/index'
36
39
  import { Route as MailBriefThreadIdMessageIdRouteImport } from './routes/mail/brief/$threadId/$messageId'
40
+ import { Route as MailOutboxDraftOutboxMessageIdRouteImport } from './routes/mail/outbox/draft/$outboxMessageId'
37
41
 
38
42
  const IndexRoute = IndexRouteImport.update({
39
43
  id: '/',
@@ -130,6 +134,11 @@ const MailMailboxIdIndexRoute = MailMailboxIdIndexRouteImport.update({
130
134
  path: '/',
131
135
  getParentRoute: () => MailMailboxIdRoute,
132
136
  } as any)
137
+ const MailMailboxIdThreadIdRoute = MailMailboxIdThreadIdRouteImport.update({
138
+ id: '/$threadId',
139
+ path: '/$threadId',
140
+ getParentRoute: () => MailMailboxIdRoute,
141
+ } as any)
133
142
  const MailBriefIndexRoute = MailBriefIndexRouteImport.update({
134
143
  id: '/',
135
144
  path: '/',
@@ -150,6 +159,18 @@ const MailOutboxIndexRoute = MailOutboxIndexRouteImport.update({
150
159
  path: '/',
151
160
  getParentRoute: () => MailOutboxRoute,
152
161
  } as any)
162
+ const MailMailboxIdThreadIdIndexRoute =
163
+ MailMailboxIdThreadIdIndexRouteImport.update({
164
+ id: '/',
165
+ path: '/',
166
+ getParentRoute: () => MailMailboxIdThreadIdRoute,
167
+ } as any)
168
+ const MailMailboxIdThreadIdMessageIdRoute =
169
+ MailMailboxIdThreadIdMessageIdRouteImport.update({
170
+ id: '/$messageId',
171
+ path: '/$messageId',
172
+ getParentRoute: () => MailMailboxIdThreadIdRoute,
173
+ } as any)
153
174
  const MailBriefThreadIdIndexRoute = MailBriefThreadIdIndexRouteImport.update({
154
175
  id: '/',
155
176
  path: '/',
@@ -161,6 +182,12 @@ const MailBriefThreadIdMessageIdRoute =
161
182
  path: '/$messageId',
162
183
  getParentRoute: () => MailBriefThreadIdRoute,
163
184
  } as any)
185
+ const MailOutboxDraftOutboxMessageIdRoute =
186
+ MailOutboxDraftOutboxMessageIdRouteImport.update({
187
+ id: '/draft/$outboxMessageId',
188
+ path: '/draft/$outboxMessageId',
189
+ getParentRoute: () => MailOutboxRoute,
190
+ } as any)
164
191
 
165
192
  export interface FileRoutesByFullPath {
166
193
  '/': typeof IndexRoute
@@ -181,12 +208,16 @@ export interface FileRoutesByFullPath {
181
208
  '/settings/suggested-vips': typeof SettingsSuggestedVipsRoute
182
209
  '/mail/': typeof MailIndexRoute
183
210
  '/settings/': typeof SettingsIndexRoute
211
+ '/mail/$mailboxId/$threadId': typeof MailMailboxIdThreadIdRouteWithChildren
184
212
  '/mail/brief/$threadId': typeof MailBriefThreadIdRouteWithChildren
185
213
  '/mail/$mailboxId/': typeof MailMailboxIdIndexRoute
186
214
  '/mail/brief/': typeof MailBriefIndexRoute
187
215
  '/mail/flagged/': typeof MailFlaggedIndexRoute
188
216
  '/mail/outbox/': typeof MailOutboxIndexRoute
217
+ '/mail/$mailboxId/$threadId/$messageId': typeof MailMailboxIdThreadIdMessageIdRoute
189
218
  '/mail/brief/$threadId/$messageId': typeof MailBriefThreadIdMessageIdRoute
219
+ '/mail/outbox/draft/$outboxMessageId': typeof MailOutboxDraftOutboxMessageIdRoute
220
+ '/mail/$mailboxId/$threadId/': typeof MailMailboxIdThreadIdIndexRoute
190
221
  '/mail/brief/$threadId/': typeof MailBriefThreadIdIndexRoute
191
222
  }
192
223
  export interface FileRoutesByTo {
@@ -206,7 +237,10 @@ export interface FileRoutesByTo {
206
237
  '/mail/brief': typeof MailBriefIndexRoute
207
238
  '/mail/flagged': typeof MailFlaggedIndexRoute
208
239
  '/mail/outbox': typeof MailOutboxIndexRoute
240
+ '/mail/$mailboxId/$threadId/$messageId': typeof MailMailboxIdThreadIdMessageIdRoute
209
241
  '/mail/brief/$threadId/$messageId': typeof MailBriefThreadIdMessageIdRoute
242
+ '/mail/outbox/draft/$outboxMessageId': typeof MailOutboxDraftOutboxMessageIdRoute
243
+ '/mail/$mailboxId/$threadId': typeof MailMailboxIdThreadIdIndexRoute
210
244
  '/mail/brief/$threadId': typeof MailBriefThreadIdIndexRoute
211
245
  }
212
246
  export interface FileRoutesById {
@@ -229,12 +263,16 @@ export interface FileRoutesById {
229
263
  '/settings/suggested-vips': typeof SettingsSuggestedVipsRoute
230
264
  '/mail/': typeof MailIndexRoute
231
265
  '/settings/': typeof SettingsIndexRoute
266
+ '/mail/$mailboxId/$threadId': typeof MailMailboxIdThreadIdRouteWithChildren
232
267
  '/mail/brief/$threadId': typeof MailBriefThreadIdRouteWithChildren
233
268
  '/mail/$mailboxId/': typeof MailMailboxIdIndexRoute
234
269
  '/mail/brief/': typeof MailBriefIndexRoute
235
270
  '/mail/flagged/': typeof MailFlaggedIndexRoute
236
271
  '/mail/outbox/': typeof MailOutboxIndexRoute
272
+ '/mail/$mailboxId/$threadId/$messageId': typeof MailMailboxIdThreadIdMessageIdRoute
237
273
  '/mail/brief/$threadId/$messageId': typeof MailBriefThreadIdMessageIdRoute
274
+ '/mail/outbox/draft/$outboxMessageId': typeof MailOutboxDraftOutboxMessageIdRoute
275
+ '/mail/$mailboxId/$threadId/': typeof MailMailboxIdThreadIdIndexRoute
238
276
  '/mail/brief/$threadId/': typeof MailBriefThreadIdIndexRoute
239
277
  }
240
278
  export interface FileRouteTypes {
@@ -258,12 +296,16 @@ export interface FileRouteTypes {
258
296
  | '/settings/suggested-vips'
259
297
  | '/mail/'
260
298
  | '/settings/'
299
+ | '/mail/$mailboxId/$threadId'
261
300
  | '/mail/brief/$threadId'
262
301
  | '/mail/$mailboxId/'
263
302
  | '/mail/brief/'
264
303
  | '/mail/flagged/'
265
304
  | '/mail/outbox/'
305
+ | '/mail/$mailboxId/$threadId/$messageId'
266
306
  | '/mail/brief/$threadId/$messageId'
307
+ | '/mail/outbox/draft/$outboxMessageId'
308
+ | '/mail/$mailboxId/$threadId/'
267
309
  | '/mail/brief/$threadId/'
268
310
  fileRoutesByTo: FileRoutesByTo
269
311
  to:
@@ -283,7 +325,10 @@ export interface FileRouteTypes {
283
325
  | '/mail/brief'
284
326
  | '/mail/flagged'
285
327
  | '/mail/outbox'
328
+ | '/mail/$mailboxId/$threadId/$messageId'
286
329
  | '/mail/brief/$threadId/$messageId'
330
+ | '/mail/outbox/draft/$outboxMessageId'
331
+ | '/mail/$mailboxId/$threadId'
287
332
  | '/mail/brief/$threadId'
288
333
  id:
289
334
  | '__root__'
@@ -305,12 +350,16 @@ export interface FileRouteTypes {
305
350
  | '/settings/suggested-vips'
306
351
  | '/mail/'
307
352
  | '/settings/'
353
+ | '/mail/$mailboxId/$threadId'
308
354
  | '/mail/brief/$threadId'
309
355
  | '/mail/$mailboxId/'
310
356
  | '/mail/brief/'
311
357
  | '/mail/flagged/'
312
358
  | '/mail/outbox/'
359
+ | '/mail/$mailboxId/$threadId/$messageId'
313
360
  | '/mail/brief/$threadId/$messageId'
361
+ | '/mail/outbox/draft/$outboxMessageId'
362
+ | '/mail/$mailboxId/$threadId/'
314
363
  | '/mail/brief/$threadId/'
315
364
  fileRoutesById: FileRoutesById
316
365
  }
@@ -456,6 +505,13 @@ declare module '@tanstack/react-router' {
456
505
  preLoaderRoute: typeof MailMailboxIdIndexRouteImport
457
506
  parentRoute: typeof MailMailboxIdRoute
458
507
  }
508
+ '/mail/$mailboxId/$threadId': {
509
+ id: '/mail/$mailboxId/$threadId'
510
+ path: '/$threadId'
511
+ fullPath: '/mail/$mailboxId/$threadId'
512
+ preLoaderRoute: typeof MailMailboxIdThreadIdRouteImport
513
+ parentRoute: typeof MailMailboxIdRoute
514
+ }
459
515
  '/mail/brief/': {
460
516
  id: '/mail/brief/'
461
517
  path: '/'
@@ -484,6 +540,20 @@ declare module '@tanstack/react-router' {
484
540
  preLoaderRoute: typeof MailOutboxIndexRouteImport
485
541
  parentRoute: typeof MailOutboxRoute
486
542
  }
543
+ '/mail/$mailboxId/$threadId/': {
544
+ id: '/mail/$mailboxId/$threadId/'
545
+ path: '/'
546
+ fullPath: '/mail/$mailboxId/$threadId/'
547
+ preLoaderRoute: typeof MailMailboxIdThreadIdIndexRouteImport
548
+ parentRoute: typeof MailMailboxIdThreadIdRoute
549
+ }
550
+ '/mail/$mailboxId/$threadId/$messageId': {
551
+ id: '/mail/$mailboxId/$threadId/$messageId'
552
+ path: '/$messageId'
553
+ fullPath: '/mail/$mailboxId/$threadId/$messageId'
554
+ preLoaderRoute: typeof MailMailboxIdThreadIdMessageIdRouteImport
555
+ parentRoute: typeof MailMailboxIdThreadIdRoute
556
+ }
487
557
  '/mail/brief/$threadId/': {
488
558
  id: '/mail/brief/$threadId/'
489
559
  path: '/'
@@ -498,14 +568,38 @@ declare module '@tanstack/react-router' {
498
568
  preLoaderRoute: typeof MailBriefThreadIdMessageIdRouteImport
499
569
  parentRoute: typeof MailBriefThreadIdRoute
500
570
  }
571
+ '/mail/outbox/draft/$outboxMessageId': {
572
+ id: '/mail/outbox/draft/$outboxMessageId'
573
+ path: '/draft/$outboxMessageId'
574
+ fullPath: '/mail/outbox/draft/$outboxMessageId'
575
+ preLoaderRoute: typeof MailOutboxDraftOutboxMessageIdRouteImport
576
+ parentRoute: typeof MailOutboxRoute
577
+ }
501
578
  }
502
579
  }
503
580
 
581
+ interface MailMailboxIdThreadIdRouteChildren {
582
+ MailMailboxIdThreadIdMessageIdRoute: typeof MailMailboxIdThreadIdMessageIdRoute
583
+ MailMailboxIdThreadIdIndexRoute: typeof MailMailboxIdThreadIdIndexRoute
584
+ }
585
+
586
+ const MailMailboxIdThreadIdRouteChildren: MailMailboxIdThreadIdRouteChildren = {
587
+ MailMailboxIdThreadIdMessageIdRoute: MailMailboxIdThreadIdMessageIdRoute,
588
+ MailMailboxIdThreadIdIndexRoute: MailMailboxIdThreadIdIndexRoute,
589
+ }
590
+
591
+ const MailMailboxIdThreadIdRouteWithChildren =
592
+ MailMailboxIdThreadIdRoute._addFileChildren(
593
+ MailMailboxIdThreadIdRouteChildren,
594
+ )
595
+
504
596
  interface MailMailboxIdRouteChildren {
597
+ MailMailboxIdThreadIdRoute: typeof MailMailboxIdThreadIdRouteWithChildren
505
598
  MailMailboxIdIndexRoute: typeof MailMailboxIdIndexRoute
506
599
  }
507
600
 
508
601
  const MailMailboxIdRouteChildren: MailMailboxIdRouteChildren = {
602
+ MailMailboxIdThreadIdRoute: MailMailboxIdThreadIdRouteWithChildren,
509
603
  MailMailboxIdIndexRoute: MailMailboxIdIndexRoute,
510
604
  }
511
605
 
@@ -554,10 +648,12 @@ const MailFlaggedRouteWithChildren = MailFlaggedRoute._addFileChildren(
554
648
 
555
649
  interface MailOutboxRouteChildren {
556
650
  MailOutboxIndexRoute: typeof MailOutboxIndexRoute
651
+ MailOutboxDraftOutboxMessageIdRoute: typeof MailOutboxDraftOutboxMessageIdRoute
557
652
  }
558
653
 
559
654
  const MailOutboxRouteChildren: MailOutboxRouteChildren = {
560
655
  MailOutboxIndexRoute: MailOutboxIndexRoute,
656
+ MailOutboxDraftOutboxMessageIdRoute: MailOutboxDraftOutboxMessageIdRoute,
561
657
  }
562
658
 
563
659
  const MailOutboxRouteWithChildren = MailOutboxRoute._addFileChildren(
@@ -0,0 +1,19 @@
1
+ /**
2
+ * /mail/$mailboxId/$threadId/$messageId — the same conversation, with one of its
3
+ * messages expanded and scrolled to.
4
+ *
5
+ * A message is not addressable on its own: `GET /messages/{messageId}` answers
6
+ * with no thread, so the pane has nothing to fetch by. The segment names which
7
+ * message inside the thread the reader pointed at, and the surface it renders is
8
+ * the thread's.
9
+ */
10
+ import { createFileRoute } from "@tanstack/react-router";
11
+ import { MailboxPane } from "@/components/mail/MailboxPane";
12
+
13
+ function MailboxMessagePane() {
14
+ return <MailboxPane.Reading />;
15
+ }
16
+
17
+ export const Route = createFileRoute("/mail/$mailboxId/$threadId/$messageId")({
18
+ component: MailboxMessagePane,
19
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The thread with no message named. The pane opens on the newest message, the
3
+ * same as for a thread whose row the reader never pointed at.
4
+ */
5
+ import { createFileRoute } from "@tanstack/react-router";
6
+ import { MailboxPane } from "@/components/mail/MailboxPane";
7
+
8
+ function MailboxThreadPane() {
9
+ return <MailboxPane.Reading />;
10
+ }
11
+
12
+ export const Route = createFileRoute("/mail/$mailboxId/$threadId/")({
13
+ component: MailboxThreadPane,
14
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * /mail/$mailboxId/$threadId — a conversation open in a folder's reading pane.
3
+ *
4
+ * The folder in the address is the list being browsed, not the thread's home:
5
+ * a cross-folder search hit opens here too, and every message it holds names
6
+ * its own mailbox. A layout, because the message segment nests under it.
7
+ */
8
+ import { createFileRoute, Outlet } from "@tanstack/react-router";
9
+
10
+ export const Route = createFileRoute("/mail/$mailboxId/$threadId")({
11
+ component: Outlet,
12
+ });