@remit/web-client 0.0.165 → 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/BriefPane.tsx +6 -5
- package/src/components/mail/DraftsView.tsx +14 -15
- package/src/components/mail/FlaggedPane.tsx +6 -5
- package/src/components/mail/MailboxPane.tsx +36 -124
- package/src/components/mail/MessageActionMenu.tsx +4 -3
- package/src/components/mail/MessageList.tsx +4 -3
- package/src/components/mail/OutboxPane.tsx +8 -13
- package/src/components/mail/SwipeableMessageRow.tsx +4 -3
- package/src/components/mail/intelligence-drawer.stories.tsx +208 -0
- 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/fragment.render.test.ts +98 -0
- package/src/routing/fragment.test.ts +54 -9
- package/src/routing/fragment.ts +37 -9
- package/src/routing/index.ts +10 -1
- package/src/routing/nav-link.tsx +5 -3
- 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
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { type IntelligenceData, IntelligencePanel } from "@remit/ui";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { ChevronLeft, Info } from "lucide-react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
6
|
+
import { Drawer } from "@/components/layout/Drawer";
|
|
7
|
+
import { resolveRailOpen } from "@/lib/intelligence-pref";
|
|
8
|
+
import { parseOpenPanels, retainOpenPanelsAtTier } from "@/routing";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The intelligence drawer below the desktop tier (#777).
|
|
12
|
+
*
|
|
13
|
+
* There is no fourth pane here: the rail is a full-screen drawer over the open
|
|
14
|
+
* message, so it belongs to the thread it was opened for. The address is the
|
|
15
|
+
* only place its open state lives, and a navigation drops it — otherwise a DKIM
|
|
16
|
+
* mismatch on one message leaves the drawer covering every message opened
|
|
17
|
+
* after it.
|
|
18
|
+
*
|
|
19
|
+
* Both rules are the app's own: the fragment goes through
|
|
20
|
+
* `retainOpenPanelsAtTier`, and what is up is `resolveRailOpen`.
|
|
21
|
+
*/
|
|
22
|
+
const meta: Meta = {
|
|
23
|
+
title: "Flows/Reading/Intelligence Drawer",
|
|
24
|
+
parameters: { layout: "centered" },
|
|
25
|
+
};
|
|
26
|
+
export default meta;
|
|
27
|
+
|
|
28
|
+
type Story = StoryObj;
|
|
29
|
+
|
|
30
|
+
const PHONE_WIDTH = 390;
|
|
31
|
+
const PHONE_HEIGHT = 720;
|
|
32
|
+
|
|
33
|
+
const retainOnPhone = retainOpenPanelsAtTier(false);
|
|
34
|
+
|
|
35
|
+
const intelligence: IntelligenceData = {
|
|
36
|
+
sender: {
|
|
37
|
+
name: "Mondial Relay",
|
|
38
|
+
email: "delivery.notice.4421@gmail.example",
|
|
39
|
+
trust: "unknown",
|
|
40
|
+
firstSeenLabel: "today",
|
|
41
|
+
inboundCount: 1,
|
|
42
|
+
replyCount: 0,
|
|
43
|
+
},
|
|
44
|
+
authenticity: {
|
|
45
|
+
verdict: "mismatch",
|
|
46
|
+
fromDomain: "mondialrelay.fr",
|
|
47
|
+
dkimDomain: "gmail.example",
|
|
48
|
+
claimedBrand: "Mondial Relay",
|
|
49
|
+
summary:
|
|
50
|
+
"The display name claims “Mondial Relay”, but the message was sent and signed by a personal gmail.example mailbox — not mondialrelay.fr.",
|
|
51
|
+
similarCount: 15,
|
|
52
|
+
},
|
|
53
|
+
category: { value: "automated" },
|
|
54
|
+
similar: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
interface Message {
|
|
58
|
+
id: string;
|
|
59
|
+
from: string;
|
|
60
|
+
subject: string;
|
|
61
|
+
body: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const messages: readonly Message[] = [
|
|
65
|
+
{
|
|
66
|
+
id: "msg-parcel",
|
|
67
|
+
from: "Mondial Relay",
|
|
68
|
+
subject: "Your parcel could not be delivered",
|
|
69
|
+
body: "We attempted to deliver your parcel today and nobody was home. Pay the outstanding €2.40 redelivery fee within 24 hours.",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "msg-standup",
|
|
73
|
+
from: "Alex Rivera",
|
|
74
|
+
subject: "Standup moved to 10:15",
|
|
75
|
+
body: "Pushing today's standup back fifteen minutes — the room is taken until then.",
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The phone reader, driven by the address alone: `hash` is the whole state,
|
|
81
|
+
* and every navigation writes it through the same helper the panes use.
|
|
82
|
+
*/
|
|
83
|
+
const PhoneReader = ({ initialHash = "" }: { initialHash?: string }) => {
|
|
84
|
+
const [hash, setHash] = useState(initialHash);
|
|
85
|
+
const [openId, setOpenId] = useState<string | undefined>(messages[0]?.id);
|
|
86
|
+
const open = messages.find((message) => message.id === openId);
|
|
87
|
+
const drawerOpen = resolveRailOpen({
|
|
88
|
+
panels: parseOpenPanels(hash),
|
|
89
|
+
prefersOpen: true,
|
|
90
|
+
isDesktop: false,
|
|
91
|
+
hasThread: open !== undefined,
|
|
92
|
+
});
|
|
93
|
+
const navigate = (nextId: string | undefined) => {
|
|
94
|
+
setHash(retainOnPhone(hash));
|
|
95
|
+
setOpenId(nextId);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="flex flex-col gap-2">
|
|
100
|
+
<div
|
|
101
|
+
className="flex flex-col overflow-hidden rounded-lg border border-line bg-canvas"
|
|
102
|
+
// The transform makes the frame a containing block, so the drawer's
|
|
103
|
+
// `position: fixed` resolves against the phone instead of the canvas.
|
|
104
|
+
style={{
|
|
105
|
+
width: PHONE_WIDTH,
|
|
106
|
+
height: PHONE_HEIGHT,
|
|
107
|
+
transform: "translateZ(0)",
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{open ? (
|
|
111
|
+
<>
|
|
112
|
+
<header className="flex items-center gap-1 border-b border-line px-2 py-2">
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
aria-label="Back"
|
|
116
|
+
onClick={() => navigate(undefined)}
|
|
117
|
+
className="inline-flex size-11 items-center justify-center rounded-md text-fg hover:bg-surface-raised"
|
|
118
|
+
>
|
|
119
|
+
<ChevronLeft className="size-5" />
|
|
120
|
+
</button>
|
|
121
|
+
<h1 className="min-w-0 flex-1 truncate text-sm font-semibold text-fg">
|
|
122
|
+
{open.subject}
|
|
123
|
+
</h1>
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
aria-label="Message details"
|
|
127
|
+
onClick={() => setHash("intelligence")}
|
|
128
|
+
className="inline-flex size-11 items-center justify-center rounded-md text-fg hover:bg-surface-raised"
|
|
129
|
+
>
|
|
130
|
+
<Info className="size-5" />
|
|
131
|
+
</button>
|
|
132
|
+
</header>
|
|
133
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-3 text-sm text-fg">
|
|
134
|
+
<p className="mb-2 text-2xs text-fg-subtle">{open.from}</p>
|
|
135
|
+
<p>{open.body}</p>
|
|
136
|
+
</div>
|
|
137
|
+
</>
|
|
138
|
+
) : (
|
|
139
|
+
<ul className="flex flex-col">
|
|
140
|
+
{messages.map((message) => (
|
|
141
|
+
<li key={message.id}>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={() => navigate(message.id)}
|
|
145
|
+
className="flex w-full flex-col items-start gap-0.5 border-b border-line px-4 py-3 text-left hover:bg-surface-raised"
|
|
146
|
+
>
|
|
147
|
+
<span className="text-sm font-semibold text-fg">
|
|
148
|
+
{message.from}
|
|
149
|
+
</span>
|
|
150
|
+
<span className="text-sm text-fg-muted">
|
|
151
|
+
{message.subject}
|
|
152
|
+
</span>
|
|
153
|
+
</button>
|
|
154
|
+
</li>
|
|
155
|
+
))}
|
|
156
|
+
</ul>
|
|
157
|
+
)}
|
|
158
|
+
<Drawer
|
|
159
|
+
isOpen={drawerOpen}
|
|
160
|
+
onClose={() => setHash("")}
|
|
161
|
+
ariaLabel="Message details"
|
|
162
|
+
side="right"
|
|
163
|
+
>
|
|
164
|
+
<IntelligencePanel data={intelligence} hideCloseButton />
|
|
165
|
+
</Drawer>
|
|
166
|
+
</div>
|
|
167
|
+
<p className="font-mono text-2xs text-fg-subtle">
|
|
168
|
+
{`/mail/inbox${open ? `/${open.id}` : ""}${hash ? `#${hash}` : ""}`}
|
|
169
|
+
</p>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/** A message with no drawer over it: the address names no panel. */
|
|
175
|
+
export const Closed: Story = {
|
|
176
|
+
render: () => <PhoneReader />,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The drawer over the message it was opened for — here by a DKIM mismatch,
|
|
181
|
+
* which opens it without being asked.
|
|
182
|
+
*/
|
|
183
|
+
export const OpenOverThread: Story = {
|
|
184
|
+
render: () => <PhoneReader initialHash="intelligence" />,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The same drawer, then Back and another message. The fragment is dropped on
|
|
189
|
+
* the way out, so the second message is not covered by a panel opened for the
|
|
190
|
+
* first. Press Back with the drawer up to walk it.
|
|
191
|
+
*/
|
|
192
|
+
export const DroppedByNavigation: Story = {
|
|
193
|
+
render: () => <PhoneReader initialHash="intelligence" />,
|
|
194
|
+
play: async ({ canvasElement }) => {
|
|
195
|
+
const canvas = within(canvasElement);
|
|
196
|
+
await userEvent.click(canvas.getByLabelText("Back"));
|
|
197
|
+
await userEvent.click(canvas.getByText("Standup moved to 10:15"));
|
|
198
|
+
await expect(canvas.queryByRole("dialog")).toBeNull();
|
|
199
|
+
await expect(canvas.getByText("/mail/inbox/msg-standup")).toBeVisible();
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/** The drawer over the dark theme. */
|
|
204
|
+
export const OpenOverThreadDark: Story = {
|
|
205
|
+
name: "Open Over Thread (dark)",
|
|
206
|
+
parameters: { theme: "dark" },
|
|
207
|
+
render: () => <PhoneReader initialHash="intelligence" />,
|
|
208
|
+
};
|
|
@@ -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
|
+
});
|