@remit/web-client 0.0.144 → 0.0.145
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/MailShell.tsx +98 -0
- package/src/components/mail/BriefPane.tsx +5 -5
- package/src/components/mail/FlaggedPane.tsx +1 -1
- package/src/components/mail/MailSidebarAdapter.tsx +5 -8
- package/src/components/mail/MailboxPane.tsx +5 -5
- package/src/components/ui/FatalErrorOverlay.tsx +4 -1
- package/src/hooks/useSearchMirror.ts +93 -0
- package/src/hooks/useSearchScope.ts +1 -1
- package/src/lib/compose-routes.test.ts +3 -1
- package/src/lib/compose-routes.ts +1 -1
- package/src/lib/mail-route.test.ts +157 -57
- package/src/lib/mail-route.ts +71 -38
- package/src/lib/mail-search.ts +51 -0
- package/src/lib/route-search-query.test.ts +50 -28
- package/src/lib/search-scope.ts +11 -19
- package/src/lib/search-view.test.ts +142 -38
- package/src/lib/search-view.ts +35 -11
- package/src/routeTree.gen.ts +155 -18
- package/src/router.tsx +17 -0
- package/src/routes/index.tsx +1 -1
- package/src/routes/mail/$mailboxId/index.tsx +10 -0
- package/src/routes/mail/$mailboxId.tsx +27 -19
- package/src/routes/mail/brief/index.tsx +14 -0
- package/src/routes/mail/brief.tsx +50 -0
- package/src/routes/mail/flagged/index.tsx +10 -0
- package/src/routes/mail/flagged.tsx +25 -13
- package/src/routes/mail/index.tsx +9 -36
- package/src/routes/mail/outbox/index.tsx +10 -0
- package/src/routes/mail/outbox.tsx +23 -13
- package/src/routes/mail.tsx +35 -246
- package/src/test-support/list-search-binding.ts +34 -0
package/src/lib/search-view.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* on, so switching mailbox clears a stale query while a deep link or a saved
|
|
11
11
|
* search that carries `q` still arrives with the query intact.
|
|
12
12
|
*/
|
|
13
|
+
import { locationIsOnList } from "./mail-route";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* The field text after a view transition, or `undefined` when nothing changes
|
|
@@ -36,19 +37,42 @@ export function committedSearchQuery(
|
|
|
36
37
|
return searchInput === "" ? "" : debouncedSearchInput;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export interface MirrorDecision {
|
|
41
|
+
/** The live field text. */
|
|
42
|
+
searchInput: string;
|
|
43
|
+
/** The debounced query the search APIs are running. */
|
|
44
|
+
committedQuery: string;
|
|
45
|
+
/** What the URL currently says the query is. */
|
|
46
|
+
urlQuery: string;
|
|
47
|
+
/** Where the router says the reader is, which is where a write would land. */
|
|
48
|
+
pathname: string;
|
|
49
|
+
/** The path of the list asking. */
|
|
50
|
+
listPath: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
/**
|
|
40
|
-
* Whether
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
54
|
+
* Whether a list may write the committed query into the URL.
|
|
55
|
+
*
|
|
56
|
+
* Only a settled query may: mid-debounce the committed value is the *previous*
|
|
57
|
+
* query, and writing it would overwrite the URL that a deep link or a saved
|
|
58
|
+
* search just arrived with — stripping `q` for as long as the debounce lasts,
|
|
59
|
+
* and taking the search with it. The mirror waits for the field and the
|
|
60
|
+
* committed query to agree, then writes only if the URL says something else.
|
|
61
|
+
*
|
|
62
|
+
* And only the list the reader is actually on may: a list stays mounted, effects
|
|
63
|
+
* and all, until the list they navigated to is ready to paint, by which time the
|
|
64
|
+
* address is already the new one. A write from the outgoing list in that window
|
|
65
|
+
* navigates back to itself, superseding the load in flight and replacing the
|
|
66
|
+
* entry the reader just pushed — they click Inbox and land on the brief.
|
|
46
67
|
*/
|
|
47
|
-
export function shouldMirrorQuery(
|
|
48
|
-
searchInput
|
|
49
|
-
committedQuery
|
|
50
|
-
urlQuery
|
|
51
|
-
|
|
68
|
+
export function shouldMirrorQuery({
|
|
69
|
+
searchInput,
|
|
70
|
+
committedQuery,
|
|
71
|
+
urlQuery,
|
|
72
|
+
pathname,
|
|
73
|
+
listPath,
|
|
74
|
+
}: MirrorDecision): boolean {
|
|
75
|
+
if (!locationIsOnList(pathname, listPath)) return false;
|
|
52
76
|
if (committedQuery !== searchInput) return false;
|
|
53
77
|
return committedQuery !== urlQuery;
|
|
54
78
|
}
|
package/src/routeTree.gen.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { Route as OnboardingRouteImport } from './routes/onboarding'
|
|
|
15
15
|
import { Route as SettingsRouteImport } from './routes/settings'
|
|
16
16
|
import { Route as MailIndexRouteImport } from './routes/mail/index'
|
|
17
17
|
import { Route as MailMailboxIdRouteImport } from './routes/mail/$mailboxId'
|
|
18
|
+
import { Route as MailBriefRouteImport } from './routes/mail/brief'
|
|
18
19
|
import { Route as MailFlaggedRouteImport } from './routes/mail/flagged'
|
|
19
20
|
import { Route as MailOutboxRouteImport } from './routes/mail/outbox'
|
|
20
21
|
import { Route as SettingsIndexRouteImport } from './routes/settings/index'
|
|
@@ -26,6 +27,10 @@ import { Route as SettingsFoldersRouteImport } from './routes/settings/folders'
|
|
|
26
27
|
import { Route as SettingsLabelsRouteImport } from './routes/settings/labels'
|
|
27
28
|
import { Route as SettingsSendersRouteImport } from './routes/settings/senders'
|
|
28
29
|
import { Route as SettingsSuggestedVipsRouteImport } from './routes/settings/suggested-vips'
|
|
30
|
+
import { Route as MailMailboxIdIndexRouteImport } from './routes/mail/$mailboxId/index'
|
|
31
|
+
import { Route as MailBriefIndexRouteImport } from './routes/mail/brief/index'
|
|
32
|
+
import { Route as MailFlaggedIndexRouteImport } from './routes/mail/flagged/index'
|
|
33
|
+
import { Route as MailOutboxIndexRouteImport } from './routes/mail/outbox/index'
|
|
29
34
|
|
|
30
35
|
const IndexRoute = IndexRouteImport.update({
|
|
31
36
|
id: '/',
|
|
@@ -57,6 +62,11 @@ const MailMailboxIdRoute = MailMailboxIdRouteImport.update({
|
|
|
57
62
|
path: '/$mailboxId',
|
|
58
63
|
getParentRoute: () => MailRoute,
|
|
59
64
|
} as any)
|
|
65
|
+
const MailBriefRoute = MailBriefRouteImport.update({
|
|
66
|
+
id: '/brief',
|
|
67
|
+
path: '/brief',
|
|
68
|
+
getParentRoute: () => MailRoute,
|
|
69
|
+
} as any)
|
|
60
70
|
const MailFlaggedRoute = MailFlaggedRouteImport.update({
|
|
61
71
|
id: '/flagged',
|
|
62
72
|
path: '/flagged',
|
|
@@ -112,15 +122,36 @@ const SettingsSuggestedVipsRoute = SettingsSuggestedVipsRouteImport.update({
|
|
|
112
122
|
path: '/suggested-vips',
|
|
113
123
|
getParentRoute: () => SettingsRoute,
|
|
114
124
|
} as any)
|
|
125
|
+
const MailMailboxIdIndexRoute = MailMailboxIdIndexRouteImport.update({
|
|
126
|
+
id: '/',
|
|
127
|
+
path: '/',
|
|
128
|
+
getParentRoute: () => MailMailboxIdRoute,
|
|
129
|
+
} as any)
|
|
130
|
+
const MailBriefIndexRoute = MailBriefIndexRouteImport.update({
|
|
131
|
+
id: '/',
|
|
132
|
+
path: '/',
|
|
133
|
+
getParentRoute: () => MailBriefRoute,
|
|
134
|
+
} as any)
|
|
135
|
+
const MailFlaggedIndexRoute = MailFlaggedIndexRouteImport.update({
|
|
136
|
+
id: '/',
|
|
137
|
+
path: '/',
|
|
138
|
+
getParentRoute: () => MailFlaggedRoute,
|
|
139
|
+
} as any)
|
|
140
|
+
const MailOutboxIndexRoute = MailOutboxIndexRouteImport.update({
|
|
141
|
+
id: '/',
|
|
142
|
+
path: '/',
|
|
143
|
+
getParentRoute: () => MailOutboxRoute,
|
|
144
|
+
} as any)
|
|
115
145
|
|
|
116
146
|
export interface FileRoutesByFullPath {
|
|
117
147
|
'/': typeof IndexRoute
|
|
118
148
|
'/mail': typeof MailRouteWithChildren
|
|
119
149
|
'/onboarding': typeof OnboardingRoute
|
|
120
150
|
'/settings': typeof SettingsRouteWithChildren
|
|
121
|
-
'/mail/$mailboxId': typeof
|
|
122
|
-
'/mail/
|
|
123
|
-
'/mail/
|
|
151
|
+
'/mail/$mailboxId': typeof MailMailboxIdRouteWithChildren
|
|
152
|
+
'/mail/brief': typeof MailBriefRouteWithChildren
|
|
153
|
+
'/mail/flagged': typeof MailFlaggedRouteWithChildren
|
|
154
|
+
'/mail/outbox': typeof MailOutboxRouteWithChildren
|
|
124
155
|
'/settings/accounts': typeof SettingsAccountsRoute
|
|
125
156
|
'/settings/advanced': typeof SettingsAdvancedRoute
|
|
126
157
|
'/settings/appearance': typeof SettingsAppearanceRoute
|
|
@@ -131,13 +162,14 @@ export interface FileRoutesByFullPath {
|
|
|
131
162
|
'/settings/suggested-vips': typeof SettingsSuggestedVipsRoute
|
|
132
163
|
'/mail/': typeof MailIndexRoute
|
|
133
164
|
'/settings/': typeof SettingsIndexRoute
|
|
165
|
+
'/mail/$mailboxId/': typeof MailMailboxIdIndexRoute
|
|
166
|
+
'/mail/brief/': typeof MailBriefIndexRoute
|
|
167
|
+
'/mail/flagged/': typeof MailFlaggedIndexRoute
|
|
168
|
+
'/mail/outbox/': typeof MailOutboxIndexRoute
|
|
134
169
|
}
|
|
135
170
|
export interface FileRoutesByTo {
|
|
136
171
|
'/': typeof IndexRoute
|
|
137
172
|
'/onboarding': typeof OnboardingRoute
|
|
138
|
-
'/mail/$mailboxId': typeof MailMailboxIdRoute
|
|
139
|
-
'/mail/flagged': typeof MailFlaggedRoute
|
|
140
|
-
'/mail/outbox': typeof MailOutboxRoute
|
|
141
173
|
'/settings/accounts': typeof SettingsAccountsRoute
|
|
142
174
|
'/settings/advanced': typeof SettingsAdvancedRoute
|
|
143
175
|
'/settings/appearance': typeof SettingsAppearanceRoute
|
|
@@ -148,6 +180,10 @@ export interface FileRoutesByTo {
|
|
|
148
180
|
'/settings/suggested-vips': typeof SettingsSuggestedVipsRoute
|
|
149
181
|
'/mail': typeof MailIndexRoute
|
|
150
182
|
'/settings': typeof SettingsIndexRoute
|
|
183
|
+
'/mail/$mailboxId': typeof MailMailboxIdIndexRoute
|
|
184
|
+
'/mail/brief': typeof MailBriefIndexRoute
|
|
185
|
+
'/mail/flagged': typeof MailFlaggedIndexRoute
|
|
186
|
+
'/mail/outbox': typeof MailOutboxIndexRoute
|
|
151
187
|
}
|
|
152
188
|
export interface FileRoutesById {
|
|
153
189
|
__root__: typeof rootRouteImport
|
|
@@ -155,9 +191,10 @@ export interface FileRoutesById {
|
|
|
155
191
|
'/mail': typeof MailRouteWithChildren
|
|
156
192
|
'/onboarding': typeof OnboardingRoute
|
|
157
193
|
'/settings': typeof SettingsRouteWithChildren
|
|
158
|
-
'/mail/$mailboxId': typeof
|
|
159
|
-
'/mail/
|
|
160
|
-
'/mail/
|
|
194
|
+
'/mail/$mailboxId': typeof MailMailboxIdRouteWithChildren
|
|
195
|
+
'/mail/brief': typeof MailBriefRouteWithChildren
|
|
196
|
+
'/mail/flagged': typeof MailFlaggedRouteWithChildren
|
|
197
|
+
'/mail/outbox': typeof MailOutboxRouteWithChildren
|
|
161
198
|
'/settings/accounts': typeof SettingsAccountsRoute
|
|
162
199
|
'/settings/advanced': typeof SettingsAdvancedRoute
|
|
163
200
|
'/settings/appearance': typeof SettingsAppearanceRoute
|
|
@@ -168,6 +205,10 @@ export interface FileRoutesById {
|
|
|
168
205
|
'/settings/suggested-vips': typeof SettingsSuggestedVipsRoute
|
|
169
206
|
'/mail/': typeof MailIndexRoute
|
|
170
207
|
'/settings/': typeof SettingsIndexRoute
|
|
208
|
+
'/mail/$mailboxId/': typeof MailMailboxIdIndexRoute
|
|
209
|
+
'/mail/brief/': typeof MailBriefIndexRoute
|
|
210
|
+
'/mail/flagged/': typeof MailFlaggedIndexRoute
|
|
211
|
+
'/mail/outbox/': typeof MailOutboxIndexRoute
|
|
171
212
|
}
|
|
172
213
|
export interface FileRouteTypes {
|
|
173
214
|
fileRoutesByFullPath: FileRoutesByFullPath
|
|
@@ -177,6 +218,7 @@ export interface FileRouteTypes {
|
|
|
177
218
|
| '/onboarding'
|
|
178
219
|
| '/settings'
|
|
179
220
|
| '/mail/$mailboxId'
|
|
221
|
+
| '/mail/brief'
|
|
180
222
|
| '/mail/flagged'
|
|
181
223
|
| '/mail/outbox'
|
|
182
224
|
| '/settings/accounts'
|
|
@@ -189,13 +231,14 @@ export interface FileRouteTypes {
|
|
|
189
231
|
| '/settings/suggested-vips'
|
|
190
232
|
| '/mail/'
|
|
191
233
|
| '/settings/'
|
|
234
|
+
| '/mail/$mailboxId/'
|
|
235
|
+
| '/mail/brief/'
|
|
236
|
+
| '/mail/flagged/'
|
|
237
|
+
| '/mail/outbox/'
|
|
192
238
|
fileRoutesByTo: FileRoutesByTo
|
|
193
239
|
to:
|
|
194
240
|
| '/'
|
|
195
241
|
| '/onboarding'
|
|
196
|
-
| '/mail/$mailboxId'
|
|
197
|
-
| '/mail/flagged'
|
|
198
|
-
| '/mail/outbox'
|
|
199
242
|
| '/settings/accounts'
|
|
200
243
|
| '/settings/advanced'
|
|
201
244
|
| '/settings/appearance'
|
|
@@ -206,6 +249,10 @@ export interface FileRouteTypes {
|
|
|
206
249
|
| '/settings/suggested-vips'
|
|
207
250
|
| '/mail'
|
|
208
251
|
| '/settings'
|
|
252
|
+
| '/mail/$mailboxId'
|
|
253
|
+
| '/mail/brief'
|
|
254
|
+
| '/mail/flagged'
|
|
255
|
+
| '/mail/outbox'
|
|
209
256
|
id:
|
|
210
257
|
| '__root__'
|
|
211
258
|
| '/'
|
|
@@ -213,6 +260,7 @@ export interface FileRouteTypes {
|
|
|
213
260
|
| '/onboarding'
|
|
214
261
|
| '/settings'
|
|
215
262
|
| '/mail/$mailboxId'
|
|
263
|
+
| '/mail/brief'
|
|
216
264
|
| '/mail/flagged'
|
|
217
265
|
| '/mail/outbox'
|
|
218
266
|
| '/settings/accounts'
|
|
@@ -225,6 +273,10 @@ export interface FileRouteTypes {
|
|
|
225
273
|
| '/settings/suggested-vips'
|
|
226
274
|
| '/mail/'
|
|
227
275
|
| '/settings/'
|
|
276
|
+
| '/mail/$mailboxId/'
|
|
277
|
+
| '/mail/brief/'
|
|
278
|
+
| '/mail/flagged/'
|
|
279
|
+
| '/mail/outbox/'
|
|
228
280
|
fileRoutesById: FileRoutesById
|
|
229
281
|
}
|
|
230
282
|
export interface RootRouteChildren {
|
|
@@ -278,6 +330,13 @@ declare module '@tanstack/react-router' {
|
|
|
278
330
|
preLoaderRoute: typeof MailMailboxIdRouteImport
|
|
279
331
|
parentRoute: typeof MailRoute
|
|
280
332
|
}
|
|
333
|
+
'/mail/brief': {
|
|
334
|
+
id: '/mail/brief'
|
|
335
|
+
path: '/brief'
|
|
336
|
+
fullPath: '/mail/brief'
|
|
337
|
+
preLoaderRoute: typeof MailBriefRouteImport
|
|
338
|
+
parentRoute: typeof MailRoute
|
|
339
|
+
}
|
|
281
340
|
'/mail/flagged': {
|
|
282
341
|
id: '/mail/flagged'
|
|
283
342
|
path: '/flagged'
|
|
@@ -355,20 +414,98 @@ declare module '@tanstack/react-router' {
|
|
|
355
414
|
preLoaderRoute: typeof SettingsSuggestedVipsRouteImport
|
|
356
415
|
parentRoute: typeof SettingsRoute
|
|
357
416
|
}
|
|
417
|
+
'/mail/$mailboxId/': {
|
|
418
|
+
id: '/mail/$mailboxId/'
|
|
419
|
+
path: '/'
|
|
420
|
+
fullPath: '/mail/$mailboxId/'
|
|
421
|
+
preLoaderRoute: typeof MailMailboxIdIndexRouteImport
|
|
422
|
+
parentRoute: typeof MailMailboxIdRoute
|
|
423
|
+
}
|
|
424
|
+
'/mail/brief/': {
|
|
425
|
+
id: '/mail/brief/'
|
|
426
|
+
path: '/'
|
|
427
|
+
fullPath: '/mail/brief/'
|
|
428
|
+
preLoaderRoute: typeof MailBriefIndexRouteImport
|
|
429
|
+
parentRoute: typeof MailBriefRoute
|
|
430
|
+
}
|
|
431
|
+
'/mail/flagged/': {
|
|
432
|
+
id: '/mail/flagged/'
|
|
433
|
+
path: '/'
|
|
434
|
+
fullPath: '/mail/flagged/'
|
|
435
|
+
preLoaderRoute: typeof MailFlaggedIndexRouteImport
|
|
436
|
+
parentRoute: typeof MailFlaggedRoute
|
|
437
|
+
}
|
|
438
|
+
'/mail/outbox/': {
|
|
439
|
+
id: '/mail/outbox/'
|
|
440
|
+
path: '/'
|
|
441
|
+
fullPath: '/mail/outbox/'
|
|
442
|
+
preLoaderRoute: typeof MailOutboxIndexRouteImport
|
|
443
|
+
parentRoute: typeof MailOutboxRoute
|
|
444
|
+
}
|
|
358
445
|
}
|
|
359
446
|
}
|
|
360
447
|
|
|
448
|
+
interface MailMailboxIdRouteChildren {
|
|
449
|
+
MailMailboxIdIndexRoute: typeof MailMailboxIdIndexRoute
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const MailMailboxIdRouteChildren: MailMailboxIdRouteChildren = {
|
|
453
|
+
MailMailboxIdIndexRoute: MailMailboxIdIndexRoute,
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const MailMailboxIdRouteWithChildren = MailMailboxIdRoute._addFileChildren(
|
|
457
|
+
MailMailboxIdRouteChildren,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
interface MailBriefRouteChildren {
|
|
461
|
+
MailBriefIndexRoute: typeof MailBriefIndexRoute
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const MailBriefRouteChildren: MailBriefRouteChildren = {
|
|
465
|
+
MailBriefIndexRoute: MailBriefIndexRoute,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const MailBriefRouteWithChildren = MailBriefRoute._addFileChildren(
|
|
469
|
+
MailBriefRouteChildren,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
interface MailFlaggedRouteChildren {
|
|
473
|
+
MailFlaggedIndexRoute: typeof MailFlaggedIndexRoute
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const MailFlaggedRouteChildren: MailFlaggedRouteChildren = {
|
|
477
|
+
MailFlaggedIndexRoute: MailFlaggedIndexRoute,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const MailFlaggedRouteWithChildren = MailFlaggedRoute._addFileChildren(
|
|
481
|
+
MailFlaggedRouteChildren,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
interface MailOutboxRouteChildren {
|
|
485
|
+
MailOutboxIndexRoute: typeof MailOutboxIndexRoute
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const MailOutboxRouteChildren: MailOutboxRouteChildren = {
|
|
489
|
+
MailOutboxIndexRoute: MailOutboxIndexRoute,
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const MailOutboxRouteWithChildren = MailOutboxRoute._addFileChildren(
|
|
493
|
+
MailOutboxRouteChildren,
|
|
494
|
+
)
|
|
495
|
+
|
|
361
496
|
interface MailRouteChildren {
|
|
362
|
-
MailMailboxIdRoute: typeof
|
|
363
|
-
|
|
364
|
-
|
|
497
|
+
MailMailboxIdRoute: typeof MailMailboxIdRouteWithChildren
|
|
498
|
+
MailBriefRoute: typeof MailBriefRouteWithChildren
|
|
499
|
+
MailFlaggedRoute: typeof MailFlaggedRouteWithChildren
|
|
500
|
+
MailOutboxRoute: typeof MailOutboxRouteWithChildren
|
|
365
501
|
MailIndexRoute: typeof MailIndexRoute
|
|
366
502
|
}
|
|
367
503
|
|
|
368
504
|
const MailRouteChildren: MailRouteChildren = {
|
|
369
|
-
MailMailboxIdRoute:
|
|
370
|
-
|
|
371
|
-
|
|
505
|
+
MailMailboxIdRoute: MailMailboxIdRouteWithChildren,
|
|
506
|
+
MailBriefRoute: MailBriefRouteWithChildren,
|
|
507
|
+
MailFlaggedRoute: MailFlaggedRouteWithChildren,
|
|
508
|
+
MailOutboxRoute: MailOutboxRouteWithChildren,
|
|
372
509
|
MailIndexRoute: MailIndexRoute,
|
|
373
510
|
}
|
|
374
511
|
|
package/src/router.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { QueryClient } from "@tanstack/react-query";
|
|
2
2
|
import { createRouter } from "@tanstack/react-router";
|
|
3
|
+
import { AppShellSkeleton } from "./components/layout/AppShellSkeleton";
|
|
3
4
|
import { recordRoute } from "./lib/route-breadcrumbs";
|
|
4
5
|
import type { Telemetry } from "./lib/telemetry";
|
|
5
6
|
import { routeTree } from "./routeTree.gen";
|
|
@@ -16,6 +17,22 @@ export const createAppRouter = (
|
|
|
16
17
|
routeTree,
|
|
17
18
|
context: { queryClient },
|
|
18
19
|
defaultPreload: "intent",
|
|
20
|
+
// A route arrives with its own code, and each mail list carries a whole pane
|
|
21
|
+
// in it. Without a pending state the router keeps the previous route on
|
|
22
|
+
// screen for that fetch and first mount, under the new address — so the list
|
|
23
|
+
// the reader has left stays interactive: a click routes through its handlers
|
|
24
|
+
// and takes them back to it, and the keyboard drives its cursor. Preloading
|
|
25
|
+
// on intent does not close that: it fires on hover and focus, so it only
|
|
26
|
+
// moves the window's start, and it never applies to the navigations the
|
|
27
|
+
// keyboard shortcuts make.
|
|
28
|
+
//
|
|
29
|
+
// 200ms is above a preloaded transition, which lands in tens of milliseconds
|
|
30
|
+
// and so paints no skeleton at all, and below the point where a stale list
|
|
31
|
+
// invites a click. Once shown it stays 300ms, so a transition that settles
|
|
32
|
+
// just after the threshold does not strobe.
|
|
33
|
+
defaultPendingComponent: AppShellSkeleton,
|
|
34
|
+
defaultPendingMs: 200,
|
|
35
|
+
defaultPendingMinMs: 300,
|
|
19
36
|
});
|
|
20
37
|
|
|
21
38
|
router.subscribe("onResolved", (event) => {
|
package/src/routes/index.tsx
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { MailboxPane } from "@/components/mail/MailboxPane";
|
|
3
|
+
|
|
4
|
+
function MailboxReadingPane() {
|
|
5
|
+
return <MailboxPane.Reading />;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const Route = createFileRoute("/mail/$mailboxId/")({
|
|
9
|
+
component: MailboxReadingPane,
|
|
10
|
+
});
|
|
@@ -1,27 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* /mail/$mailboxId
|
|
2
|
+
* /mail/$mailboxId — one folder, the fullest of the four list layouts: list,
|
|
3
|
+
* reading pane and intelligence rail.
|
|
3
4
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
* `<MailboxPane>` directly into `<AppShellSlotted>` slots. TanStack Router
|
|
7
|
-
* still needs this file to:
|
|
8
|
-
* - Register the route so URL matching works
|
|
9
|
-
* - Validate the search params so selectedMessageId is properly typed
|
|
5
|
+
* The literal lists are declared as siblings and TanStack matches literals
|
|
6
|
+
* first, so a mailbox id can never be read as one of them.
|
|
10
7
|
*/
|
|
11
|
-
import { createFileRoute } from "@tanstack/react-router";
|
|
12
|
-
import {
|
|
8
|
+
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
|
9
|
+
import { MailShell } from "@/components/layout/MailShell";
|
|
10
|
+
import { MailboxPane } from "@/components/mail/MailboxPane";
|
|
11
|
+
import { useSearchMirror } from "@/hooks/useSearchMirror";
|
|
12
|
+
import { mailboxSearchSchema } from "@/lib/mail-search";
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
selectedMessageId
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
14
|
+
function MailboxLayout() {
|
|
15
|
+
const { mailboxId } = Route.useParams();
|
|
16
|
+
const { selectedMessageId } = Route.useSearch();
|
|
17
|
+
useSearchMirror({ to: "/mail/$mailboxId", params: { mailboxId } });
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<MailboxPane mailboxId={mailboxId} selectedMessageId={selectedMessageId}>
|
|
21
|
+
<MailShell
|
|
22
|
+
phone={<MailboxPane.Phone />}
|
|
23
|
+
list={<MailboxPane.List />}
|
|
24
|
+
reading={<Outlet />}
|
|
25
|
+
intelligence={<MailboxPane.Intelligence />}
|
|
26
|
+
hasThread={Boolean(selectedMessageId)}
|
|
27
|
+
/>
|
|
28
|
+
</MailboxPane>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
23
31
|
|
|
24
32
|
export const Route = createFileRoute("/mail/$mailboxId")({
|
|
25
|
-
component:
|
|
33
|
+
component: MailboxLayout,
|
|
26
34
|
validateSearch: mailboxSearchSchema,
|
|
27
35
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The brief's reading pane. It is a route because the pane belongs to the list
|
|
3
|
+
* it was opened from; the thread and message segments arrive under it.
|
|
4
|
+
*/
|
|
5
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
6
|
+
import { BriefPane } from "@/components/mail/BriefPane";
|
|
7
|
+
|
|
8
|
+
function BriefReadingPane() {
|
|
9
|
+
return <BriefPane.Reading />;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Route = createFileRoute("/mail/brief/")({
|
|
13
|
+
component: BriefReadingPane,
|
|
14
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /mail/brief — the daily brief, one of the four list layouts.
|
|
3
|
+
*
|
|
4
|
+
* The route mounts the list and the shell around it, and the reading pane is
|
|
5
|
+
* the `Outlet`, so what the pane shows is whatever route is matched under this
|
|
6
|
+
* one rather than a decision made somewhere above.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
createFileRoute,
|
|
10
|
+
type ErrorComponentProps,
|
|
11
|
+
Outlet,
|
|
12
|
+
} from "@tanstack/react-router";
|
|
13
|
+
import { MailShell } from "@/components/layout/MailShell";
|
|
14
|
+
import { BriefPane } from "@/components/mail/BriefPane";
|
|
15
|
+
import { ErrorState } from "@/components/ui/ErrorState";
|
|
16
|
+
import { useSearchMirror } from "@/hooks/useSearchMirror";
|
|
17
|
+
import { briefSearchSchema } from "@/lib/mail-search";
|
|
18
|
+
|
|
19
|
+
const BriefError = ({ error, reset }: ErrorComponentProps) => (
|
|
20
|
+
<div className="flex h-full items-center justify-center bg-canvas p-4">
|
|
21
|
+
<ErrorState
|
|
22
|
+
title="Couldn't load your mailboxes"
|
|
23
|
+
error={error}
|
|
24
|
+
onRetry={reset}
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
function BriefLayout() {
|
|
30
|
+
const { selectedMessageId } = Route.useSearch();
|
|
31
|
+
useSearchMirror({ to: "/mail/brief" });
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<BriefPane selectedMessageId={selectedMessageId}>
|
|
35
|
+
<MailShell
|
|
36
|
+
phone={<BriefPane.Phone />}
|
|
37
|
+
list={<BriefPane.List />}
|
|
38
|
+
reading={<Outlet />}
|
|
39
|
+
intelligence={<BriefPane.Intelligence />}
|
|
40
|
+
hasThread={Boolean(selectedMessageId)}
|
|
41
|
+
/>
|
|
42
|
+
</BriefPane>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Route = createFileRoute("/mail/brief")({
|
|
47
|
+
component: BriefLayout,
|
|
48
|
+
validateSearch: briefSearchSchema,
|
|
49
|
+
errorComponent: BriefError,
|
|
50
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { FlaggedPane } from "@/components/mail/FlaggedPane";
|
|
3
|
+
|
|
4
|
+
function FlaggedReadingPane() {
|
|
5
|
+
return <FlaggedPane.Reading />;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const Route = createFileRoute("/mail/flagged/")({
|
|
9
|
+
component: FlaggedReadingPane,
|
|
10
|
+
});
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* /mail/flagged
|
|
2
|
+
* /mail/flagged — the Flagged virtual mailbox, one of the four list layouts.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* directly into `<AppShellSlotted>` slots — the same pattern the brief uses.
|
|
4
|
+
* A flat starred list across accounts; same slots as the brief, intelligence
|
|
5
|
+
* rail included.
|
|
7
6
|
*/
|
|
8
7
|
import {
|
|
9
8
|
createFileRoute,
|
|
10
9
|
type ErrorComponentProps,
|
|
10
|
+
Outlet,
|
|
11
11
|
} from "@tanstack/react-router";
|
|
12
|
-
import {
|
|
12
|
+
import { MailShell } from "@/components/layout/MailShell";
|
|
13
|
+
import { FlaggedPane } from "@/components/mail/FlaggedPane";
|
|
13
14
|
import { ErrorState } from "@/components/ui/ErrorState";
|
|
15
|
+
import { useSearchMirror } from "@/hooks/useSearchMirror";
|
|
16
|
+
import { flaggedSearchSchema } from "@/lib/mail-search";
|
|
14
17
|
|
|
15
18
|
const FlaggedError = ({ error, reset }: ErrorComponentProps) => (
|
|
16
19
|
<div className="flex h-full items-center justify-center bg-canvas p-4">
|
|
@@ -22,16 +25,25 @@ const FlaggedError = ({ error, reset }: ErrorComponentProps) => (
|
|
|
22
25
|
</div>
|
|
23
26
|
);
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
function FlaggedLayout() {
|
|
29
|
+
const { selectedMessageId } = Route.useSearch();
|
|
30
|
+
useSearchMirror({ to: "/mail/flagged" });
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<FlaggedPane selectedMessageId={selectedMessageId}>
|
|
34
|
+
<MailShell
|
|
35
|
+
phone={<FlaggedPane.Phone />}
|
|
36
|
+
list={<FlaggedPane.List />}
|
|
37
|
+
reading={<Outlet />}
|
|
38
|
+
intelligence={<FlaggedPane.Intelligence />}
|
|
39
|
+
hasThread={Boolean(selectedMessageId)}
|
|
40
|
+
/>
|
|
41
|
+
</FlaggedPane>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
32
44
|
|
|
33
45
|
export const Route = createFileRoute("/mail/flagged")({
|
|
34
|
-
component:
|
|
46
|
+
component: FlaggedLayout,
|
|
35
47
|
validateSearch: flaggedSearchSchema,
|
|
36
48
|
errorComponent: FlaggedError,
|
|
37
49
|
});
|
|
@@ -1,41 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* which detects the brief route via `useRouterState` and mounts
|
|
6
|
-
* `<BriefPane>` directly into `<AppShellSlotted>` slots.
|
|
2
|
+
* `/mail` has no view of its own: the daily brief is one of the four lists and
|
|
3
|
+
* lives at its own path, so every link into the mail app that names no list
|
|
4
|
+
* lands here and is sent on to the brief carrying whatever it arrived with.
|
|
7
5
|
*/
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
type ErrorComponentProps,
|
|
11
|
-
} from "@tanstack/react-router";
|
|
12
|
-
import { z } from "zod";
|
|
13
|
-
import { ErrorState } from "@/components/ui/ErrorState";
|
|
14
|
-
|
|
15
|
-
const MailIndexError = ({ error, reset }: ErrorComponentProps) => (
|
|
16
|
-
<div className="flex h-full items-center justify-center bg-canvas p-4">
|
|
17
|
-
<ErrorState
|
|
18
|
-
title="Couldn't load your mailboxes"
|
|
19
|
-
error={error}
|
|
20
|
-
onRetry={reset}
|
|
21
|
-
/>
|
|
22
|
-
</div>
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
// `q` is inherited from the parent /mail route; re-declared here so it
|
|
26
|
-
// survives this route's own search validation and isn't dropped when
|
|
27
|
-
// navigating with a functional search updater.
|
|
28
|
-
const briefSearchSchema = z.object({
|
|
29
|
-
selectedMessageId: z.string().optional(),
|
|
30
|
-
// A tapped semantic "Related" hit can point at a message outside the loaded
|
|
31
|
-
// brief list; carrying its thread + mailbox lets the brief open it directly.
|
|
32
|
-
selectedThreadId: z.string().optional(),
|
|
33
|
-
selectedMailboxId: z.string().optional(),
|
|
34
|
-
q: z.string().optional(),
|
|
35
|
-
});
|
|
6
|
+
import { createFileRoute, redirect } from "@tanstack/react-router";
|
|
7
|
+
import { mailIndexSearchSchema } from "@/lib/mail-search";
|
|
36
8
|
|
|
37
9
|
export const Route = createFileRoute("/mail/")({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
10
|
+
validateSearch: mailIndexSearchSchema,
|
|
11
|
+
beforeLoad: ({ search }) => {
|
|
12
|
+
throw redirect({ to: "/mail/brief", search, replace: true });
|
|
13
|
+
},
|
|
41
14
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { OutboxPane } from "@/components/mail/OutboxPane";
|
|
3
|
+
|
|
4
|
+
function OutboxReadingPane() {
|
|
5
|
+
return <OutboxPane.Reading />;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const Route = createFileRoute("/mail/outbox/")({
|
|
9
|
+
component: OutboxReadingPane,
|
|
10
|
+
});
|