@remit/web-client 0.0.166 → 0.0.167
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/compose/ComposeForm.tsx +77 -25
- package/src/components/compose/ComposeProvider.tsx +11 -122
- package/src/components/compose/FullCompose.tsx +29 -18
- package/src/components/compose/InlineCompose.tsx +21 -11
- package/src/components/compose/MobileComposeSheet.tsx +14 -18
- package/src/components/compose/compose-send-stops-autosave.render.test.ts +7 -9
- package/src/components/compose/compose-starts-a-second-message.render.test.ts +170 -0
- package/src/components/compose/compose-title.ts +10 -0
- package/src/components/compose/mobile-header-stays-expanded.render.test.ts +5 -13
- package/src/components/layout/ComposeFab.tsx +6 -15
- package/src/components/layout/MailShell.tsx +9 -1
- package/src/components/layout/MailTopBar.tsx +3 -6
- package/src/components/mail/DraftsView.tsx +12 -14
- package/src/components/mail/MailboxPane.tsx +27 -117
- package/src/components/mail/OutboxPane.tsx +3 -9
- package/src/hooks/useSaveDraft.ts +11 -0
- package/src/hooks/useSearchMirror.ts +12 -1
- package/src/lib/mail-route.test.ts +35 -1
- package/src/lib/mail-route.ts +2 -1
- package/src/routeTree.gen.ts +92 -0
- package/src/routes/mail/$mailboxId/compose.{-$outboxMessageId}.tsx +14 -0
- package/src/routes/mail/brief/compose.{-$outboxMessageId}.tsx +22 -0
- package/src/routes/mail/flagged/compose.{-$outboxMessageId}.tsx +13 -0
- package/src/routes/mail/outbox/compose.{-$outboxMessageId}.tsx +17 -0
- package/src/routes/mail.tsx +12 -8
- package/src/routing/compose-press-opens.render.test.ts +179 -0
- package/src/routing/compose.ts +220 -0
- package/src/routing/index.ts +8 -0
- package/src/components/compose/compose-clears-open-thread.render.test.ts +0 -312
- package/src/hooks/useComposeTargetMailbox.ts +0 -75
- package/src/lib/compose-routes.test.ts +0 -46
- package/src/lib/compose-routes.ts +0 -25
|
@@ -48,7 +48,6 @@ import {
|
|
|
48
48
|
useMemo,
|
|
49
49
|
useRef,
|
|
50
50
|
} from "react";
|
|
51
|
-
import { useCompose } from "@/components/compose/ComposeProvider";
|
|
52
51
|
import { MessageBody } from "@/components/mail/MessageBody";
|
|
53
52
|
import { NavMenuButton } from "@/components/mail/NavMenuButton";
|
|
54
53
|
import { useErrorBanners } from "@/components/ui/ErrorBannerProvider";
|
|
@@ -63,7 +62,7 @@ import {
|
|
|
63
62
|
import { isOutboxListRow, isUnsendableStatus } from "@/lib/outbox-status";
|
|
64
63
|
import { normalizeSearchQuery } from "@/lib/search-query";
|
|
65
64
|
import { parseSearchTokens } from "@/lib/search-tokens";
|
|
66
|
-
import { useRetainOpenPanels } from "@/routing";
|
|
65
|
+
import { useEditDraft, useRetainOpenPanels } from "@/routing";
|
|
67
66
|
|
|
68
67
|
/* ------------------------------------------------------------------ */
|
|
69
68
|
/* Helpers (shared between List, Reading, Phone sub-views) */
|
|
@@ -266,7 +265,7 @@ function OutboxMessageRow({
|
|
|
266
265
|
onSelect,
|
|
267
266
|
}: OutboxMessageRowProps) {
|
|
268
267
|
const queryClient = useQueryClient();
|
|
269
|
-
const
|
|
268
|
+
const editDraft = useEditDraft();
|
|
270
269
|
const { pushError } = useErrorBanners();
|
|
271
270
|
|
|
272
271
|
const invalidateOutbox = useCallback(() => {
|
|
@@ -331,12 +330,7 @@ function OutboxMessageRow({
|
|
|
331
330
|
: undefined
|
|
332
331
|
}
|
|
333
332
|
retrying={retryMutation.isPending}
|
|
334
|
-
onEdit={() =>
|
|
335
|
-
openCompose({
|
|
336
|
-
mode: "new",
|
|
337
|
-
outboxMessageId: message.outboxMessageId,
|
|
338
|
-
})
|
|
339
|
-
}
|
|
333
|
+
onEdit={() => editDraft(message.outboxMessageId)}
|
|
340
334
|
onDelete={() =>
|
|
341
335
|
deleteMutation.mutate({
|
|
342
336
|
path: { outboxMessageId: message.outboxMessageId },
|
|
@@ -52,8 +52,19 @@ export const useSaveDraft = ({
|
|
|
52
52
|
const propIdRef = useRef(outboxMessageId);
|
|
53
53
|
const targetIdRef = useRef(outboxMessageId);
|
|
54
54
|
if (propIdRef.current !== outboxMessageId) {
|
|
55
|
+
// A write waiting out the debounce holds the document that has just been
|
|
56
|
+
// left, aimed at whatever entry is current when it fires. Leaving one is
|
|
57
|
+
// therefore dropping it: otherwise it lands on the next draft, or — with
|
|
58
|
+
// no entry to land on at all — creates one holding the last message's
|
|
59
|
+
// content, which is what a second Compose press used to do.
|
|
60
|
+
//
|
|
61
|
+
// Adoption is the exception, as it is everywhere: the id arriving is this
|
|
62
|
+
// session's own draft coming back, and a save scheduled while it was in
|
|
63
|
+
// flight is still the right content for it.
|
|
64
|
+
const leavingADocument = propIdRef.current !== undefined;
|
|
55
65
|
propIdRef.current = outboxMessageId;
|
|
56
66
|
targetIdRef.current = outboxMessageId;
|
|
67
|
+
if (leavingADocument && timerRef.current) clearTimeout(timerRef.current);
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
const createMutation = useMutation(
|
|
@@ -2,6 +2,7 @@ import { useNavigate, useRouterState, useSearch } from "@tanstack/react-router";
|
|
|
2
2
|
import { useEffect, useRef } from "react";
|
|
3
3
|
import { useMailContext } from "@/lib/mail-context";
|
|
4
4
|
import { shouldMirrorQuery } from "@/lib/search-view";
|
|
5
|
+
import { useIsComposing } from "@/routing";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* The list the mirror writes to. Each list calls the hook with its own route,
|
|
@@ -40,12 +41,17 @@ export type SearchMirrorTarget =
|
|
|
40
41
|
* was opened under it (not a pre-search leftover) and must survive. The close
|
|
41
42
|
* otherwise raced the tap: the row shows before the debounce settles, so this
|
|
42
43
|
* mirror can land just after the open and undo it.
|
|
44
|
+
*
|
|
45
|
+
* A composer is not the pre-search list's leftover either. It is the reader's
|
|
46
|
+
* own unsent message, so a search started while it is up narrows the list
|
|
47
|
+
* behind it and leaves it standing.
|
|
43
48
|
*/
|
|
44
49
|
export function useSearchMirror(target: SearchMirrorTarget): void {
|
|
45
50
|
const navigate = useNavigate();
|
|
46
51
|
const { searchInput, searchQuery: committedQuery } = useMailContext();
|
|
47
52
|
const { q: urlQuery = "" } = useSearch({ from: "/mail" });
|
|
48
53
|
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
54
|
+
const isComposing = useIsComposing();
|
|
49
55
|
|
|
50
56
|
// Read at effect time rather than depended on: the URL is what the mirror
|
|
51
57
|
// compares against, not what re-triggers it.
|
|
@@ -67,13 +73,17 @@ export function useSearchMirror(target: SearchMirrorTarget): void {
|
|
|
67
73
|
if (!mayWrite) return;
|
|
68
74
|
const queryGoesActive =
|
|
69
75
|
Boolean(committedQuery) && urlQueryRef.current !== committedQuery;
|
|
76
|
+
// The composer is the reader's own unsent message, not a leftover of the
|
|
77
|
+
// list they were on, so a query going active narrows the list behind it and
|
|
78
|
+
// leaves it where it is.
|
|
79
|
+
const closesTheOpenSurface = queryGoesActive && !isComposing;
|
|
70
80
|
const search = (prev: Record<string, unknown>) => ({
|
|
71
81
|
...prev,
|
|
72
82
|
q: committedQuery || undefined,
|
|
73
83
|
});
|
|
74
84
|
// A query is a mode of the view the reader is already in, so the panels
|
|
75
85
|
// they have up are not ones they navigated away from.
|
|
76
|
-
if (!
|
|
86
|
+
if (!closesTheOpenSurface) {
|
|
77
87
|
navigate({ to: ".", search, hash: true, replace: true });
|
|
78
88
|
return;
|
|
79
89
|
}
|
|
@@ -97,5 +107,6 @@ export function useSearchMirror(target: SearchMirrorTarget): void {
|
|
|
97
107
|
mailboxId,
|
|
98
108
|
listPath,
|
|
99
109
|
pathname,
|
|
110
|
+
isComposing,
|
|
100
111
|
]);
|
|
101
112
|
}
|
|
@@ -131,6 +131,34 @@ describe("mailViewKey", () => {
|
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
// Same trap one surface over: compose is a child of the list, and a key that
|
|
135
|
+
// moved when it opened would wipe the query the reader was mid-search on.
|
|
136
|
+
it("gives a list and its compose surface the same key", () => {
|
|
137
|
+
assert.equal(
|
|
138
|
+
mailViewKey(
|
|
139
|
+
matches([
|
|
140
|
+
MAIL_BRIEF_ROUTE_ID,
|
|
141
|
+
`${MAIL_BRIEF_ROUTE_ID}/compose/{-$outboxMessageId}`,
|
|
142
|
+
]),
|
|
143
|
+
),
|
|
144
|
+
mailViewKey(brief),
|
|
145
|
+
);
|
|
146
|
+
assert.equal(
|
|
147
|
+
mailViewKey(
|
|
148
|
+
matches(
|
|
149
|
+
[
|
|
150
|
+
MAIL_MAILBOX_ROUTE_ID,
|
|
151
|
+
`${MAIL_MAILBOX_ROUTE_ID}/compose/{-$outboxMessageId}`,
|
|
152
|
+
],
|
|
153
|
+
{
|
|
154
|
+
mailboxId: "inbox-1",
|
|
155
|
+
},
|
|
156
|
+
),
|
|
157
|
+
),
|
|
158
|
+
mailViewKey(mailbox),
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
134
162
|
it("gives a list and its reading-pane index child the same key", () => {
|
|
135
163
|
assert.equal(
|
|
136
164
|
mailViewKey(matches([MAIL_BRIEF_ROUTE_ID, `${MAIL_BRIEF_ROUTE_ID}/`])),
|
|
@@ -157,7 +185,7 @@ describe("locationIsOnList", () => {
|
|
|
157
185
|
);
|
|
158
186
|
assert.equal(locationIsOnList("/mail/brief/", "/mail/brief"), true);
|
|
159
187
|
assert.equal(
|
|
160
|
-
locationIsOnList("/mail/inbox-1/compose", "/mail/inbox-1"),
|
|
188
|
+
locationIsOnList("/mail/inbox-1/compose/ob-1", "/mail/inbox-1"),
|
|
161
189
|
true,
|
|
162
190
|
);
|
|
163
191
|
});
|
|
@@ -191,6 +219,12 @@ describe("locationOpensDetail", () => {
|
|
|
191
219
|
assert.equal(locationOpensDetail("/mail/inbox-1/thread-1"), true);
|
|
192
220
|
});
|
|
193
221
|
|
|
222
|
+
it("is true for the compose surface, which the FAB must not sit over", () => {
|
|
223
|
+
assert.equal(locationOpensDetail("/mail/brief/compose"), true);
|
|
224
|
+
assert.equal(locationOpensDetail("/mail/inbox-1/compose"), true);
|
|
225
|
+
assert.equal(locationOpensDetail("/mail/inbox-1/compose/ob-1"), true);
|
|
226
|
+
});
|
|
227
|
+
|
|
194
228
|
it("is false on a bare list, trailing slash and query included", () => {
|
|
195
229
|
assert.equal(locationOpensDetail("/mail/brief"), false);
|
|
196
230
|
assert.equal(locationOpensDetail("/mail/brief/"), false);
|
package/src/lib/mail-route.ts
CHANGED
|
@@ -95,7 +95,8 @@ export function locationIsOnList(pathname: string, listPath: string): boolean {
|
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Whether the address names something the list has open below it — a thread, a
|
|
98
|
-
* message. A list on its own, and its bare reading pane,
|
|
98
|
+
* message, the compose surface. A list on its own, and its bare reading pane,
|
|
99
|
+
* do not.
|
|
99
100
|
*
|
|
100
101
|
* Answers what an open thread used to be asked of the query. Anything sharing
|
|
101
102
|
* the single pane with the conversation reads it from here rather than keeping a
|
package/src/routeTree.gen.ts
CHANGED
|
@@ -36,10 +36,14 @@ import { Route as MailFlaggedThreadIdRouteImport } from './routes/mail/flagged/$
|
|
|
36
36
|
import { Route as MailOutboxIndexRouteImport } from './routes/mail/outbox/index'
|
|
37
37
|
import { Route as MailMailboxIdThreadIdIndexRouteImport } from './routes/mail/$mailboxId/$threadId/index'
|
|
38
38
|
import { Route as MailMailboxIdThreadIdMessageIdRouteImport } from './routes/mail/$mailboxId/$threadId/$messageId'
|
|
39
|
+
import { Route as MailMailboxIdComposeChar123OutboxMessageIdChar125RouteImport } from './routes/mail/$mailboxId/compose.{-$outboxMessageId}'
|
|
39
40
|
import { Route as MailBriefThreadIdIndexRouteImport } from './routes/mail/brief/$threadId/index'
|
|
40
41
|
import { Route as MailBriefThreadIdMessageIdRouteImport } from './routes/mail/brief/$threadId/$messageId'
|
|
42
|
+
import { Route as MailBriefComposeChar123OutboxMessageIdChar125RouteImport } from './routes/mail/brief/compose.{-$outboxMessageId}'
|
|
41
43
|
import { Route as MailFlaggedThreadIdIndexRouteImport } from './routes/mail/flagged/$threadId/index'
|
|
42
44
|
import { Route as MailFlaggedThreadIdMessageIdRouteImport } from './routes/mail/flagged/$threadId/$messageId'
|
|
45
|
+
import { Route as MailFlaggedComposeChar123OutboxMessageIdChar125RouteImport } from './routes/mail/flagged/compose.{-$outboxMessageId}'
|
|
46
|
+
import { Route as MailOutboxComposeChar123OutboxMessageIdChar125RouteImport } from './routes/mail/outbox/compose.{-$outboxMessageId}'
|
|
43
47
|
import { Route as MailOutboxDraftOutboxMessageIdRouteImport } from './routes/mail/outbox/draft/$outboxMessageId'
|
|
44
48
|
|
|
45
49
|
const IndexRoute = IndexRouteImport.update({
|
|
@@ -179,6 +183,12 @@ const MailMailboxIdThreadIdMessageIdRoute =
|
|
|
179
183
|
path: '/$messageId',
|
|
180
184
|
getParentRoute: () => MailMailboxIdThreadIdRoute,
|
|
181
185
|
} as any)
|
|
186
|
+
const MailMailboxIdComposeChar123OutboxMessageIdChar125Route =
|
|
187
|
+
MailMailboxIdComposeChar123OutboxMessageIdChar125RouteImport.update({
|
|
188
|
+
id: '/compose/{-$outboxMessageId}',
|
|
189
|
+
path: '/compose/{-$outboxMessageId}',
|
|
190
|
+
getParentRoute: () => MailMailboxIdRoute,
|
|
191
|
+
} as any)
|
|
182
192
|
const MailBriefThreadIdIndexRoute = MailBriefThreadIdIndexRouteImport.update({
|
|
183
193
|
id: '/',
|
|
184
194
|
path: '/',
|
|
@@ -190,6 +200,12 @@ const MailBriefThreadIdMessageIdRoute =
|
|
|
190
200
|
path: '/$messageId',
|
|
191
201
|
getParentRoute: () => MailBriefThreadIdRoute,
|
|
192
202
|
} as any)
|
|
203
|
+
const MailBriefComposeChar123OutboxMessageIdChar125Route =
|
|
204
|
+
MailBriefComposeChar123OutboxMessageIdChar125RouteImport.update({
|
|
205
|
+
id: '/compose/{-$outboxMessageId}',
|
|
206
|
+
path: '/compose/{-$outboxMessageId}',
|
|
207
|
+
getParentRoute: () => MailBriefRoute,
|
|
208
|
+
} as any)
|
|
193
209
|
const MailFlaggedThreadIdIndexRoute =
|
|
194
210
|
MailFlaggedThreadIdIndexRouteImport.update({
|
|
195
211
|
id: '/',
|
|
@@ -202,6 +218,18 @@ const MailFlaggedThreadIdMessageIdRoute =
|
|
|
202
218
|
path: '/$messageId',
|
|
203
219
|
getParentRoute: () => MailFlaggedThreadIdRoute,
|
|
204
220
|
} as any)
|
|
221
|
+
const MailFlaggedComposeChar123OutboxMessageIdChar125Route =
|
|
222
|
+
MailFlaggedComposeChar123OutboxMessageIdChar125RouteImport.update({
|
|
223
|
+
id: '/compose/{-$outboxMessageId}',
|
|
224
|
+
path: '/compose/{-$outboxMessageId}',
|
|
225
|
+
getParentRoute: () => MailFlaggedRoute,
|
|
226
|
+
} as any)
|
|
227
|
+
const MailOutboxComposeChar123OutboxMessageIdChar125Route =
|
|
228
|
+
MailOutboxComposeChar123OutboxMessageIdChar125RouteImport.update({
|
|
229
|
+
id: '/compose/{-$outboxMessageId}',
|
|
230
|
+
path: '/compose/{-$outboxMessageId}',
|
|
231
|
+
getParentRoute: () => MailOutboxRoute,
|
|
232
|
+
} as any)
|
|
205
233
|
const MailOutboxDraftOutboxMessageIdRoute =
|
|
206
234
|
MailOutboxDraftOutboxMessageIdRouteImport.update({
|
|
207
235
|
id: '/draft/$outboxMessageId',
|
|
@@ -236,8 +264,12 @@ export interface FileRoutesByFullPath {
|
|
|
236
264
|
'/mail/flagged/': typeof MailFlaggedIndexRoute
|
|
237
265
|
'/mail/outbox/': typeof MailOutboxIndexRoute
|
|
238
266
|
'/mail/$mailboxId/$threadId/$messageId': typeof MailMailboxIdThreadIdMessageIdRoute
|
|
267
|
+
'/mail/$mailboxId/compose/{-$outboxMessageId}': typeof MailMailboxIdComposeChar123OutboxMessageIdChar125Route
|
|
239
268
|
'/mail/brief/$threadId/$messageId': typeof MailBriefThreadIdMessageIdRoute
|
|
269
|
+
'/mail/brief/compose/{-$outboxMessageId}': typeof MailBriefComposeChar123OutboxMessageIdChar125Route
|
|
240
270
|
'/mail/flagged/$threadId/$messageId': typeof MailFlaggedThreadIdMessageIdRoute
|
|
271
|
+
'/mail/flagged/compose/{-$outboxMessageId}': typeof MailFlaggedComposeChar123OutboxMessageIdChar125Route
|
|
272
|
+
'/mail/outbox/compose/{-$outboxMessageId}': typeof MailOutboxComposeChar123OutboxMessageIdChar125Route
|
|
241
273
|
'/mail/outbox/draft/$outboxMessageId': typeof MailOutboxDraftOutboxMessageIdRoute
|
|
242
274
|
'/mail/$mailboxId/$threadId/': typeof MailMailboxIdThreadIdIndexRoute
|
|
243
275
|
'/mail/brief/$threadId/': typeof MailBriefThreadIdIndexRoute
|
|
@@ -261,8 +293,12 @@ export interface FileRoutesByTo {
|
|
|
261
293
|
'/mail/flagged': typeof MailFlaggedIndexRoute
|
|
262
294
|
'/mail/outbox': typeof MailOutboxIndexRoute
|
|
263
295
|
'/mail/$mailboxId/$threadId/$messageId': typeof MailMailboxIdThreadIdMessageIdRoute
|
|
296
|
+
'/mail/$mailboxId/compose/{-$outboxMessageId}': typeof MailMailboxIdComposeChar123OutboxMessageIdChar125Route
|
|
264
297
|
'/mail/brief/$threadId/$messageId': typeof MailBriefThreadIdMessageIdRoute
|
|
298
|
+
'/mail/brief/compose/{-$outboxMessageId}': typeof MailBriefComposeChar123OutboxMessageIdChar125Route
|
|
265
299
|
'/mail/flagged/$threadId/$messageId': typeof MailFlaggedThreadIdMessageIdRoute
|
|
300
|
+
'/mail/flagged/compose/{-$outboxMessageId}': typeof MailFlaggedComposeChar123OutboxMessageIdChar125Route
|
|
301
|
+
'/mail/outbox/compose/{-$outboxMessageId}': typeof MailOutboxComposeChar123OutboxMessageIdChar125Route
|
|
266
302
|
'/mail/outbox/draft/$outboxMessageId': typeof MailOutboxDraftOutboxMessageIdRoute
|
|
267
303
|
'/mail/$mailboxId/$threadId': typeof MailMailboxIdThreadIdIndexRoute
|
|
268
304
|
'/mail/brief/$threadId': typeof MailBriefThreadIdIndexRoute
|
|
@@ -296,8 +332,12 @@ export interface FileRoutesById {
|
|
|
296
332
|
'/mail/flagged/': typeof MailFlaggedIndexRoute
|
|
297
333
|
'/mail/outbox/': typeof MailOutboxIndexRoute
|
|
298
334
|
'/mail/$mailboxId/$threadId/$messageId': typeof MailMailboxIdThreadIdMessageIdRoute
|
|
335
|
+
'/mail/$mailboxId/compose/{-$outboxMessageId}': typeof MailMailboxIdComposeChar123OutboxMessageIdChar125Route
|
|
299
336
|
'/mail/brief/$threadId/$messageId': typeof MailBriefThreadIdMessageIdRoute
|
|
337
|
+
'/mail/brief/compose/{-$outboxMessageId}': typeof MailBriefComposeChar123OutboxMessageIdChar125Route
|
|
300
338
|
'/mail/flagged/$threadId/$messageId': typeof MailFlaggedThreadIdMessageIdRoute
|
|
339
|
+
'/mail/flagged/compose/{-$outboxMessageId}': typeof MailFlaggedComposeChar123OutboxMessageIdChar125Route
|
|
340
|
+
'/mail/outbox/compose/{-$outboxMessageId}': typeof MailOutboxComposeChar123OutboxMessageIdChar125Route
|
|
301
341
|
'/mail/outbox/draft/$outboxMessageId': typeof MailOutboxDraftOutboxMessageIdRoute
|
|
302
342
|
'/mail/$mailboxId/$threadId/': typeof MailMailboxIdThreadIdIndexRoute
|
|
303
343
|
'/mail/brief/$threadId/': typeof MailBriefThreadIdIndexRoute
|
|
@@ -332,8 +372,12 @@ export interface FileRouteTypes {
|
|
|
332
372
|
| '/mail/flagged/'
|
|
333
373
|
| '/mail/outbox/'
|
|
334
374
|
| '/mail/$mailboxId/$threadId/$messageId'
|
|
375
|
+
| '/mail/$mailboxId/compose/{-$outboxMessageId}'
|
|
335
376
|
| '/mail/brief/$threadId/$messageId'
|
|
377
|
+
| '/mail/brief/compose/{-$outboxMessageId}'
|
|
336
378
|
| '/mail/flagged/$threadId/$messageId'
|
|
379
|
+
| '/mail/flagged/compose/{-$outboxMessageId}'
|
|
380
|
+
| '/mail/outbox/compose/{-$outboxMessageId}'
|
|
337
381
|
| '/mail/outbox/draft/$outboxMessageId'
|
|
338
382
|
| '/mail/$mailboxId/$threadId/'
|
|
339
383
|
| '/mail/brief/$threadId/'
|
|
@@ -357,8 +401,12 @@ export interface FileRouteTypes {
|
|
|
357
401
|
| '/mail/flagged'
|
|
358
402
|
| '/mail/outbox'
|
|
359
403
|
| '/mail/$mailboxId/$threadId/$messageId'
|
|
404
|
+
| '/mail/$mailboxId/compose/{-$outboxMessageId}'
|
|
360
405
|
| '/mail/brief/$threadId/$messageId'
|
|
406
|
+
| '/mail/brief/compose/{-$outboxMessageId}'
|
|
361
407
|
| '/mail/flagged/$threadId/$messageId'
|
|
408
|
+
| '/mail/flagged/compose/{-$outboxMessageId}'
|
|
409
|
+
| '/mail/outbox/compose/{-$outboxMessageId}'
|
|
362
410
|
| '/mail/outbox/draft/$outboxMessageId'
|
|
363
411
|
| '/mail/$mailboxId/$threadId'
|
|
364
412
|
| '/mail/brief/$threadId'
|
|
@@ -391,8 +439,12 @@ export interface FileRouteTypes {
|
|
|
391
439
|
| '/mail/flagged/'
|
|
392
440
|
| '/mail/outbox/'
|
|
393
441
|
| '/mail/$mailboxId/$threadId/$messageId'
|
|
442
|
+
| '/mail/$mailboxId/compose/{-$outboxMessageId}'
|
|
394
443
|
| '/mail/brief/$threadId/$messageId'
|
|
444
|
+
| '/mail/brief/compose/{-$outboxMessageId}'
|
|
395
445
|
| '/mail/flagged/$threadId/$messageId'
|
|
446
|
+
| '/mail/flagged/compose/{-$outboxMessageId}'
|
|
447
|
+
| '/mail/outbox/compose/{-$outboxMessageId}'
|
|
396
448
|
| '/mail/outbox/draft/$outboxMessageId'
|
|
397
449
|
| '/mail/$mailboxId/$threadId/'
|
|
398
450
|
| '/mail/brief/$threadId/'
|
|
@@ -597,6 +649,13 @@ declare module '@tanstack/react-router' {
|
|
|
597
649
|
preLoaderRoute: typeof MailMailboxIdThreadIdMessageIdRouteImport
|
|
598
650
|
parentRoute: typeof MailMailboxIdThreadIdRoute
|
|
599
651
|
}
|
|
652
|
+
'/mail/$mailboxId/compose/{-$outboxMessageId}': {
|
|
653
|
+
id: '/mail/$mailboxId/compose/{-$outboxMessageId}'
|
|
654
|
+
path: '/compose/{-$outboxMessageId}'
|
|
655
|
+
fullPath: '/mail/$mailboxId/compose/{-$outboxMessageId}'
|
|
656
|
+
preLoaderRoute: typeof MailMailboxIdComposeChar123OutboxMessageIdChar125RouteImport
|
|
657
|
+
parentRoute: typeof MailMailboxIdRoute
|
|
658
|
+
}
|
|
600
659
|
'/mail/brief/$threadId/': {
|
|
601
660
|
id: '/mail/brief/$threadId/'
|
|
602
661
|
path: '/'
|
|
@@ -611,6 +670,13 @@ declare module '@tanstack/react-router' {
|
|
|
611
670
|
preLoaderRoute: typeof MailBriefThreadIdMessageIdRouteImport
|
|
612
671
|
parentRoute: typeof MailBriefThreadIdRoute
|
|
613
672
|
}
|
|
673
|
+
'/mail/brief/compose/{-$outboxMessageId}': {
|
|
674
|
+
id: '/mail/brief/compose/{-$outboxMessageId}'
|
|
675
|
+
path: '/compose/{-$outboxMessageId}'
|
|
676
|
+
fullPath: '/mail/brief/compose/{-$outboxMessageId}'
|
|
677
|
+
preLoaderRoute: typeof MailBriefComposeChar123OutboxMessageIdChar125RouteImport
|
|
678
|
+
parentRoute: typeof MailBriefRoute
|
|
679
|
+
}
|
|
614
680
|
'/mail/flagged/$threadId/': {
|
|
615
681
|
id: '/mail/flagged/$threadId/'
|
|
616
682
|
path: '/'
|
|
@@ -625,6 +691,20 @@ declare module '@tanstack/react-router' {
|
|
|
625
691
|
preLoaderRoute: typeof MailFlaggedThreadIdMessageIdRouteImport
|
|
626
692
|
parentRoute: typeof MailFlaggedThreadIdRoute
|
|
627
693
|
}
|
|
694
|
+
'/mail/flagged/compose/{-$outboxMessageId}': {
|
|
695
|
+
id: '/mail/flagged/compose/{-$outboxMessageId}'
|
|
696
|
+
path: '/compose/{-$outboxMessageId}'
|
|
697
|
+
fullPath: '/mail/flagged/compose/{-$outboxMessageId}'
|
|
698
|
+
preLoaderRoute: typeof MailFlaggedComposeChar123OutboxMessageIdChar125RouteImport
|
|
699
|
+
parentRoute: typeof MailFlaggedRoute
|
|
700
|
+
}
|
|
701
|
+
'/mail/outbox/compose/{-$outboxMessageId}': {
|
|
702
|
+
id: '/mail/outbox/compose/{-$outboxMessageId}'
|
|
703
|
+
path: '/compose/{-$outboxMessageId}'
|
|
704
|
+
fullPath: '/mail/outbox/compose/{-$outboxMessageId}'
|
|
705
|
+
preLoaderRoute: typeof MailOutboxComposeChar123OutboxMessageIdChar125RouteImport
|
|
706
|
+
parentRoute: typeof MailOutboxRoute
|
|
707
|
+
}
|
|
628
708
|
'/mail/outbox/draft/$outboxMessageId': {
|
|
629
709
|
id: '/mail/outbox/draft/$outboxMessageId'
|
|
630
710
|
path: '/draft/$outboxMessageId'
|
|
@@ -653,11 +733,14 @@ const MailMailboxIdThreadIdRouteWithChildren =
|
|
|
653
733
|
interface MailMailboxIdRouteChildren {
|
|
654
734
|
MailMailboxIdThreadIdRoute: typeof MailMailboxIdThreadIdRouteWithChildren
|
|
655
735
|
MailMailboxIdIndexRoute: typeof MailMailboxIdIndexRoute
|
|
736
|
+
MailMailboxIdComposeChar123OutboxMessageIdChar125Route: typeof MailMailboxIdComposeChar123OutboxMessageIdChar125Route
|
|
656
737
|
}
|
|
657
738
|
|
|
658
739
|
const MailMailboxIdRouteChildren: MailMailboxIdRouteChildren = {
|
|
659
740
|
MailMailboxIdThreadIdRoute: MailMailboxIdThreadIdRouteWithChildren,
|
|
660
741
|
MailMailboxIdIndexRoute: MailMailboxIdIndexRoute,
|
|
742
|
+
MailMailboxIdComposeChar123OutboxMessageIdChar125Route:
|
|
743
|
+
MailMailboxIdComposeChar123OutboxMessageIdChar125Route,
|
|
661
744
|
}
|
|
662
745
|
|
|
663
746
|
const MailMailboxIdRouteWithChildren = MailMailboxIdRoute._addFileChildren(
|
|
@@ -680,11 +763,14 @@ const MailBriefThreadIdRouteWithChildren =
|
|
|
680
763
|
interface MailBriefRouteChildren {
|
|
681
764
|
MailBriefThreadIdRoute: typeof MailBriefThreadIdRouteWithChildren
|
|
682
765
|
MailBriefIndexRoute: typeof MailBriefIndexRoute
|
|
766
|
+
MailBriefComposeChar123OutboxMessageIdChar125Route: typeof MailBriefComposeChar123OutboxMessageIdChar125Route
|
|
683
767
|
}
|
|
684
768
|
|
|
685
769
|
const MailBriefRouteChildren: MailBriefRouteChildren = {
|
|
686
770
|
MailBriefThreadIdRoute: MailBriefThreadIdRouteWithChildren,
|
|
687
771
|
MailBriefIndexRoute: MailBriefIndexRoute,
|
|
772
|
+
MailBriefComposeChar123OutboxMessageIdChar125Route:
|
|
773
|
+
MailBriefComposeChar123OutboxMessageIdChar125Route,
|
|
688
774
|
}
|
|
689
775
|
|
|
690
776
|
const MailBriefRouteWithChildren = MailBriefRoute._addFileChildren(
|
|
@@ -707,11 +793,14 @@ const MailFlaggedThreadIdRouteWithChildren =
|
|
|
707
793
|
interface MailFlaggedRouteChildren {
|
|
708
794
|
MailFlaggedThreadIdRoute: typeof MailFlaggedThreadIdRouteWithChildren
|
|
709
795
|
MailFlaggedIndexRoute: typeof MailFlaggedIndexRoute
|
|
796
|
+
MailFlaggedComposeChar123OutboxMessageIdChar125Route: typeof MailFlaggedComposeChar123OutboxMessageIdChar125Route
|
|
710
797
|
}
|
|
711
798
|
|
|
712
799
|
const MailFlaggedRouteChildren: MailFlaggedRouteChildren = {
|
|
713
800
|
MailFlaggedThreadIdRoute: MailFlaggedThreadIdRouteWithChildren,
|
|
714
801
|
MailFlaggedIndexRoute: MailFlaggedIndexRoute,
|
|
802
|
+
MailFlaggedComposeChar123OutboxMessageIdChar125Route:
|
|
803
|
+
MailFlaggedComposeChar123OutboxMessageIdChar125Route,
|
|
715
804
|
}
|
|
716
805
|
|
|
717
806
|
const MailFlaggedRouteWithChildren = MailFlaggedRoute._addFileChildren(
|
|
@@ -720,11 +809,14 @@ const MailFlaggedRouteWithChildren = MailFlaggedRoute._addFileChildren(
|
|
|
720
809
|
|
|
721
810
|
interface MailOutboxRouteChildren {
|
|
722
811
|
MailOutboxIndexRoute: typeof MailOutboxIndexRoute
|
|
812
|
+
MailOutboxComposeChar123OutboxMessageIdChar125Route: typeof MailOutboxComposeChar123OutboxMessageIdChar125Route
|
|
723
813
|
MailOutboxDraftOutboxMessageIdRoute: typeof MailOutboxDraftOutboxMessageIdRoute
|
|
724
814
|
}
|
|
725
815
|
|
|
726
816
|
const MailOutboxRouteChildren: MailOutboxRouteChildren = {
|
|
727
817
|
MailOutboxIndexRoute: MailOutboxIndexRoute,
|
|
818
|
+
MailOutboxComposeChar123OutboxMessageIdChar125Route:
|
|
819
|
+
MailOutboxComposeChar123OutboxMessageIdChar125Route,
|
|
728
820
|
MailOutboxDraftOutboxMessageIdRoute: MailOutboxDraftOutboxMessageIdRoute,
|
|
729
821
|
}
|
|
730
822
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// biome-ignore lint/style/useFilenamingConvention: TanStack Router convention
|
|
2
|
+
/**
|
|
3
|
+
* /mail/$mailboxId/compose — the compose surface, open over a folder, and
|
|
4
|
+
* /mail/$mailboxId/compose/<outboxMessageId> for the draft it is editing. The
|
|
5
|
+
* Drafts folder's own rows open here.
|
|
6
|
+
*/
|
|
7
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
8
|
+
import { FullCompose } from "@/components/compose/FullCompose";
|
|
9
|
+
|
|
10
|
+
export const Route = createFileRoute(
|
|
11
|
+
"/mail/$mailboxId/compose/{-$outboxMessageId}",
|
|
12
|
+
)({
|
|
13
|
+
component: FullCompose,
|
|
14
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// biome-ignore lint/style/useFilenamingConvention: TanStack Router convention
|
|
2
|
+
/**
|
|
3
|
+
* /mail/brief/compose — the compose surface, open over the daily brief, and
|
|
4
|
+
* /mail/brief/compose/<outboxMessageId> once it has a draft to write to.
|
|
5
|
+
*
|
|
6
|
+
* A sibling of the thread route, so navigating here unmatches whatever the list
|
|
7
|
+
* had open in the same transition: nothing has to be closed first, and "opened"
|
|
8
|
+
* and "rendered" are one fact.
|
|
9
|
+
*
|
|
10
|
+
* The draft is an optional segment of this one route rather than a child route,
|
|
11
|
+
* because the first autosave adopts the id it just created while the reader is
|
|
12
|
+
* still typing. Two routes would unmount the composer mid-sentence; one route
|
|
13
|
+
* with a param that arrives is a rewritten address and nothing more.
|
|
14
|
+
*/
|
|
15
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
16
|
+
import { FullCompose } from "@/components/compose/FullCompose";
|
|
17
|
+
|
|
18
|
+
export const Route = createFileRoute("/mail/brief/compose/{-$outboxMessageId}")(
|
|
19
|
+
{
|
|
20
|
+
component: FullCompose,
|
|
21
|
+
},
|
|
22
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// biome-ignore lint/style/useFilenamingConvention: TanStack Router convention
|
|
2
|
+
/**
|
|
3
|
+
* /mail/flagged/compose — the compose surface, open over the starred list, and
|
|
4
|
+
* /mail/flagged/compose/<outboxMessageId> once it has a draft to write to.
|
|
5
|
+
*/
|
|
6
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
7
|
+
import { FullCompose } from "@/components/compose/FullCompose";
|
|
8
|
+
|
|
9
|
+
export const Route = createFileRoute(
|
|
10
|
+
"/mail/flagged/compose/{-$outboxMessageId}",
|
|
11
|
+
)({
|
|
12
|
+
component: FullCompose,
|
|
13
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// biome-ignore lint/style/useFilenamingConvention: TanStack Router convention
|
|
2
|
+
/**
|
|
3
|
+
* /mail/outbox/compose — the compose surface, open over the outbox, and
|
|
4
|
+
* /mail/outbox/compose/<outboxMessageId> for the draft it is editing.
|
|
5
|
+
*
|
|
6
|
+
* `draft/<outboxMessageId>` beside it reads an outbox message; this writes one.
|
|
7
|
+
* The two are separate addresses because they are separate surfaces, and "Edit
|
|
8
|
+
* as draft" is the move from one to the other.
|
|
9
|
+
*/
|
|
10
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
11
|
+
import { FullCompose } from "@/components/compose/FullCompose";
|
|
12
|
+
|
|
13
|
+
export const Route = createFileRoute(
|
|
14
|
+
"/mail/outbox/compose/{-$outboxMessageId}",
|
|
15
|
+
)({
|
|
16
|
+
component: FullCompose,
|
|
17
|
+
});
|
package/src/routes/mail.tsx
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
} from "@tanstack/react-router";
|
|
14
14
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
15
15
|
import { z } from "zod";
|
|
16
|
-
import { useCompose } from "@/components/compose/ComposeProvider";
|
|
17
16
|
import { ComposeFab } from "@/components/layout/ComposeFab";
|
|
18
17
|
import { MailShellProvider } from "@/components/layout/MailShell";
|
|
19
18
|
import { MailTopBar } from "@/components/layout/MailTopBar";
|
|
@@ -26,7 +25,6 @@ import { isSinglePaneTier, useLayoutTier } from "@/hooks/useLayoutTier";
|
|
|
26
25
|
import { useMailboxNameIndex } from "@/hooks/useMailboxNameIndex";
|
|
27
26
|
import { useResultFolderIndex } from "@/hooks/useResultFolderIndex";
|
|
28
27
|
import { useStaleAccountSync } from "@/hooks/useStaleAccountSync";
|
|
29
|
-
import { hostsComposeSurface } from "@/lib/compose-routes";
|
|
30
28
|
import {
|
|
31
29
|
readIntelligencePref,
|
|
32
30
|
resolveRailOpen,
|
|
@@ -34,13 +32,15 @@ import {
|
|
|
34
32
|
} from "@/lib/intelligence-pref";
|
|
35
33
|
import { MailContext } from "@/lib/mail-context";
|
|
36
34
|
import { MailFreshnessProvider } from "@/lib/mail-freshness";
|
|
37
|
-
import { mailViewKey } from "@/lib/mail-route";
|
|
35
|
+
import { mailListRoute, mailViewKey } from "@/lib/mail-route";
|
|
38
36
|
import { buildAccountNameIndex } from "@/lib/search-token-index";
|
|
39
37
|
import { committedSearchQuery, searchInputForView } from "@/lib/search-view";
|
|
40
38
|
import { wizardEntryValue, wizardStepValue } from "@/lib/wizard-history";
|
|
41
39
|
import {
|
|
42
40
|
isOverlayPanel,
|
|
43
41
|
type OverlayPanel,
|
|
42
|
+
useIsComposing,
|
|
43
|
+
useOpenCompose,
|
|
44
44
|
useOpenPanels,
|
|
45
45
|
useOpenThreadPath,
|
|
46
46
|
useSetOpenPanels,
|
|
@@ -166,7 +166,6 @@ function MailLayout() {
|
|
|
166
166
|
// the render before committing and nothing is painted with the stale query.
|
|
167
167
|
// An effect would commit one frame carrying the previous view's text, which
|
|
168
168
|
// the mirror then has to be defended against.
|
|
169
|
-
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
170
169
|
const viewKey = useRouterState({ select: (s) => mailViewKey(s.matches) });
|
|
171
170
|
const [searchViewKey, setSearchViewKey] = useState(viewKey);
|
|
172
171
|
if (searchViewKey !== viewKey) {
|
|
@@ -214,14 +213,19 @@ function MailLayout() {
|
|
|
214
213
|
|
|
215
214
|
// `c` / ⌘N off the mailbox routes — the brief, Flagged, the outbox. On a
|
|
216
215
|
// mailbox the pane's own triage layer owns the key, so this is disabled there
|
|
217
|
-
// rather than firing a second compose alongside it
|
|
218
|
-
|
|
216
|
+
// rather than firing a second compose alongside it, and while the composer is
|
|
217
|
+
// up the key belongs to whatever is being typed.
|
|
218
|
+
const openCompose = useOpenCompose();
|
|
219
|
+
const isComposing = useIsComposing();
|
|
220
|
+
const onMailbox = useRouterState({
|
|
221
|
+
select: (s) => mailListRoute(s.matches)?.list === "mailbox",
|
|
222
|
+
});
|
|
219
223
|
const composeHandlers = useMemo(
|
|
220
|
-
() => ({ compose:
|
|
224
|
+
() => ({ compose: openCompose }),
|
|
221
225
|
[openCompose],
|
|
222
226
|
);
|
|
223
227
|
useTriageKeyboard({
|
|
224
|
-
enabled: !
|
|
228
|
+
enabled: !onMailbox && !isComposing,
|
|
225
229
|
handlers: composeHandlers,
|
|
226
230
|
});
|
|
227
231
|
|