@remit/web-client 0.0.150 → 0.0.152
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/ComposeProvider.tsx +27 -5
- package/src/components/layout/ComposeFab.tsx +3 -2
- package/src/components/mail/BriefPane.tsx +8 -8
- package/src/components/mail/DailyBrief.tsx +3 -7
- package/src/components/mail/DraftsView.tsx +7 -3
- package/src/components/mail/IntelligencePane.tsx +4 -3
- package/src/components/mail/MailListHeader.tsx +1 -5
- package/src/components/mail/MailSidebarAdapter.tsx +3 -2
- package/src/components/mail/MailboxPane.tsx +130 -150
- package/src/components/mail/MessageActionMenu.tsx +12 -8
- package/src/components/mail/MessageList.selection.test.ts +6 -7
- package/src/components/mail/MessageList.tsx +28 -32
- package/src/components/mail/MessageListItem.tsx +2 -1
- package/src/components/mail/MessageRow.tsx +13 -10
- package/src/components/mail/SwipeableMessageRow.modifier-select.test.ts +8 -3
- package/src/components/mail/SwipeableMessageRow.tsx +8 -12
- package/src/hooks/useIntelligenceData.ts +1 -0
- package/src/hooks/useLayoutTier.ts +8 -5
- package/src/hooks/useMediaQuery.ts +2 -29
- package/src/lib/conversation-target.ts +7 -49
- package/src/lib/mail-search.ts +7 -8
- package/src/routeTree.gen.ts +74 -0
- package/src/routes/mail/$mailboxId/$threadId/$messageId.tsx +19 -0
- package/src/routes/mail/$mailboxId/$threadId/index.tsx +14 -0
- package/src/routes/mail/$mailboxId/$threadId.tsx +12 -0
- package/src/routes/mail/$mailboxId.tsx +7 -3
- package/src/routes/mail/brief.tsx +2 -2
- package/src/routing/index.ts +5 -5
- package/src/routing/open-thread.ts +58 -0
- package/src/lib/conversation-target.test.ts +0 -68
- package/src/lib/search-pending.test.ts +0 -99
- package/src/lib/search-pending.ts +0 -49
- package/src/routing/brief-thread.ts +0 -47
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* The mailbox list, the daily brief and Flagged used to carry three
|
|
5
5
|
* near-identical rows (#149). They differ in two axes only, both props here:
|
|
6
6
|
*
|
|
7
|
-
* - `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* - `link` — set for the mailbox list, whose rows are real links so a plain
|
|
8
|
+
* click routes and a middle click opens a tab. Omitted for the brief and
|
|
9
|
+
* Flagged, whose rows report the tap through `onClick`.
|
|
10
10
|
* - `selection` — set where multi-select is available. Absent renders the
|
|
11
11
|
* avatar alone with no checkbox, which is what "non-selectable mode" means.
|
|
12
12
|
*/
|
|
@@ -73,8 +73,11 @@ export interface MessageRowProps {
|
|
|
73
73
|
* orphan `option` is invalid ARIA and costs the row its button semantics.
|
|
74
74
|
*/
|
|
75
75
|
inListbox?: boolean;
|
|
76
|
-
/**
|
|
77
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The address the row links to: the folder being browsed and the conversation
|
|
78
|
+
* this row opens. Omit for callback-driven rows.
|
|
79
|
+
*/
|
|
80
|
+
link?: { mailboxId: string; threadId: string };
|
|
78
81
|
/** Called when the row is opened by a plain click on a non-linking row. */
|
|
79
82
|
onClick?: () => void;
|
|
80
83
|
/** Called when the row takes DOM focus, so the roving cursor follows it. */
|
|
@@ -91,7 +94,7 @@ const MessageRowComponent = ({
|
|
|
91
94
|
badge,
|
|
92
95
|
selection: selectionProp,
|
|
93
96
|
inListbox = false,
|
|
94
|
-
|
|
97
|
+
link,
|
|
95
98
|
onClick,
|
|
96
99
|
onFocusRow: onFocusRowProp,
|
|
97
100
|
}: MessageRowProps) => {
|
|
@@ -240,7 +243,7 @@ const MessageRowComponent = ({
|
|
|
240
243
|
/>
|
|
241
244
|
);
|
|
242
245
|
|
|
243
|
-
if (
|
|
246
|
+
if (link === undefined) {
|
|
244
247
|
return (
|
|
245
248
|
<button type="button" {...interactionProps} className={className}>
|
|
246
249
|
{body}
|
|
@@ -250,9 +253,9 @@ const MessageRowComponent = ({
|
|
|
250
253
|
|
|
251
254
|
return (
|
|
252
255
|
<NavLink
|
|
253
|
-
to="/mail/$mailboxId"
|
|
254
|
-
params={{
|
|
255
|
-
search={(prev) =>
|
|
256
|
+
to="/mail/$mailboxId/$threadId/$messageId"
|
|
257
|
+
params={{ ...link, messageId }}
|
|
258
|
+
search={(prev) => prev}
|
|
256
259
|
variant="row"
|
|
257
260
|
{...interactionProps}
|
|
258
261
|
className={className}
|
|
@@ -64,6 +64,10 @@ const mailRoute = createRoute({
|
|
|
64
64
|
getParentRoute: () => rootRoute,
|
|
65
65
|
path: "/mail/$mailboxId",
|
|
66
66
|
});
|
|
67
|
+
const messageRoute = createRoute({
|
|
68
|
+
getParentRoute: () => mailRoute,
|
|
69
|
+
path: "$threadId/$messageId",
|
|
70
|
+
});
|
|
67
71
|
|
|
68
72
|
interface Mounted {
|
|
69
73
|
row: HTMLElement;
|
|
@@ -74,7 +78,7 @@ interface Mounted {
|
|
|
74
78
|
const mountRow = (selectionTakesIt = true): Mounted => {
|
|
75
79
|
const selects: SelectionModifiers[] = [];
|
|
76
80
|
const router = createRouter({
|
|
77
|
-
routeTree: rootRoute.addChildren([mailRoute]),
|
|
81
|
+
routeTree: rootRoute.addChildren([mailRoute.addChildren([messageRoute])]),
|
|
78
82
|
history: createMemoryHistory({ initialEntries: ["/mail/mbx-1"] }),
|
|
79
83
|
}) as unknown as AnyRouter;
|
|
80
84
|
const created = createDomHarness({ viewportWidth: HALF_SCREEN_WIDTH });
|
|
@@ -148,9 +152,10 @@ const click = (
|
|
|
148
152
|
}),
|
|
149
153
|
);
|
|
150
154
|
|
|
155
|
+
// The conversation the row opened is the address, so the message it named is the
|
|
156
|
+
// last segment of the path.
|
|
151
157
|
const openedMessageId = (router: AnyRouter): string | undefined =>
|
|
152
|
-
|
|
153
|
-
.selectedMessageId;
|
|
158
|
+
router.state.location.pathname.split("/")[4];
|
|
154
159
|
|
|
155
160
|
describe("SwipeableMessageRow — modifier selection below the desktop width", () => {
|
|
156
161
|
it("takes a shift-press for selection instead of opening the message", async () => {
|
|
@@ -13,11 +13,6 @@ import { formatEmailDate } from "@/lib/format";
|
|
|
13
13
|
import { MessageListItem } from "./MessageListItem";
|
|
14
14
|
import { useModifierSelect } from "./useModifierSelect";
|
|
15
15
|
|
|
16
|
-
interface MailboxLinkSearch {
|
|
17
|
-
selectedMessageId?: string;
|
|
18
|
-
q?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
16
|
interface SwipeableMessageRowProps {
|
|
22
17
|
thread: RemitImapThreadMessageResponse;
|
|
23
18
|
mailboxId: string;
|
|
@@ -103,14 +98,15 @@ export const SwipeableMessageRow = ({
|
|
|
103
98
|
|
|
104
99
|
const handleOpen = useCallback(() => {
|
|
105
100
|
navigate({
|
|
106
|
-
to: "/mail/$mailboxId",
|
|
107
|
-
params: {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
101
|
+
to: "/mail/$mailboxId/$threadId/$messageId",
|
|
102
|
+
params: {
|
|
103
|
+
mailboxId,
|
|
104
|
+
threadId: thread.threadId,
|
|
105
|
+
messageId: thread.messageId,
|
|
106
|
+
},
|
|
107
|
+
search: (prev) => prev,
|
|
112
108
|
});
|
|
113
|
-
}, [navigate, mailboxId, thread.messageId]);
|
|
109
|
+
}, [navigate, mailboxId, thread.threadId, thread.messageId]);
|
|
114
110
|
|
|
115
111
|
const modifierSelect = useModifierSelect(thread.messageId, onRowSelect);
|
|
116
112
|
|
|
@@ -363,6 +363,7 @@ export function useIntelligenceData(
|
|
|
363
363
|
.filter((r) => r.messageId !== thread.messageId)
|
|
364
364
|
.map((r) => ({
|
|
365
365
|
id: r.messageId,
|
|
366
|
+
threadId: r.threadId,
|
|
366
367
|
// Prefer the mailbox the user is already viewing when the message also
|
|
367
368
|
// lives there, so opening a similar message stays in the same folder;
|
|
368
369
|
// otherwise fall back to the first mailbox it lives in.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
DESKTOP_MEDIA_QUERY,
|
|
3
|
+
DESKTOP_MIN_WIDTH,
|
|
4
|
+
useMatchMedia,
|
|
5
|
+
} from "@remit/ui";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Responsive layout tiers for the mail shell:
|
|
@@ -46,11 +49,11 @@ export const isSinglePaneTier = (tier: LayoutTier): boolean =>
|
|
|
46
49
|
|
|
47
50
|
/**
|
|
48
51
|
* Returns the current layout tier. SSR-safe (falls back to `phone` before
|
|
49
|
-
* hydration, matching `
|
|
52
|
+
* hydration, matching `useMatchMedia`'s server default).
|
|
50
53
|
*/
|
|
51
54
|
export const useLayoutTier = (): LayoutTier => {
|
|
52
|
-
const isTabletUp =
|
|
53
|
-
const isDesktop =
|
|
55
|
+
const isTabletUp = useMatchMedia(`(min-width: ${TABLET_MIN_WIDTH}px)`);
|
|
56
|
+
const isDesktop = useMatchMedia(DESKTOP_MEDIA_QUERY);
|
|
54
57
|
if (isDesktop) return "desktop";
|
|
55
58
|
if (isTabletUp) return "tablet";
|
|
56
59
|
return "phone";
|
|
@@ -1,31 +1,4 @@
|
|
|
1
|
-
import { DESKTOP_MEDIA_QUERY } from "@remit/ui";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Subscribes to a CSS media query and returns its current `matches` value.
|
|
6
|
-
* SSR-safe: returns `false` on the server / initial render before hydration.
|
|
7
|
-
*
|
|
8
|
-
* Layout breakpoints are exported by `@remit/ui` alongside the Tailwind
|
|
9
|
-
* variants they mirror — pass those, not a hand-written width.
|
|
10
|
-
*/
|
|
11
|
-
export const useMediaQuery = (query: string): boolean => {
|
|
12
|
-
const [matches, setMatches] = useState(() => {
|
|
13
|
-
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
14
|
-
return window.matchMedia(query).matches;
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
19
|
-
const mql = window.matchMedia(query);
|
|
20
|
-
const handler = (event: MediaQueryListEvent) => setMatches(event.matches);
|
|
21
|
-
// Sync state on mount in case the SSR/initial render disagreed.
|
|
22
|
-
setMatches(mql.matches);
|
|
23
|
-
mql.addEventListener("change", handler);
|
|
24
|
-
return () => mql.removeEventListener("change", handler);
|
|
25
|
-
}, [query]);
|
|
26
|
-
|
|
27
|
-
return matches;
|
|
28
|
-
};
|
|
1
|
+
import { DESKTOP_MEDIA_QUERY, useMatchMedia } from "@remit/ui";
|
|
29
2
|
|
|
30
3
|
/**
|
|
31
4
|
* True at desktop (Tailwind `lg:` and up): at least 1024px wide and not a
|
|
@@ -37,4 +10,4 @@ export const useMediaQuery = (query: string): boolean => {
|
|
|
37
10
|
* `lg` variant is redefined in `@remit/ui`'s token sheet with the same
|
|
38
11
|
* condition — change `DESKTOP_MEDIA_QUERY` and both move together.
|
|
39
12
|
*/
|
|
40
|
-
export const useIsDesktop = (): boolean =>
|
|
13
|
+
export const useIsDesktop = (): boolean => useMatchMedia(DESKTOP_MEDIA_QUERY);
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* What the reading pane needs to show a conversation.
|
|
3
|
+
*
|
|
4
|
+
* The thread is the whole address — `ConversationView` fetches by it — and the
|
|
5
|
+
* rest is what the list already knows about the row the reader pointed at, so
|
|
6
|
+
* the pane paints a subject before the thread's own messages arrive. A thread no
|
|
7
|
+
* list holds carries none of it and still opens.
|
|
7
8
|
*/
|
|
8
|
-
import type {
|
|
9
|
-
RemitImapMessageAuthenticity,
|
|
10
|
-
RemitImapThreadMessageResponse,
|
|
11
|
-
} from "@remit/api-http-client/types.gen.ts";
|
|
9
|
+
import type { RemitImapMessageAuthenticity } from "@remit/api-http-client/types.gen.ts";
|
|
12
10
|
|
|
13
11
|
export interface ConversationTarget {
|
|
14
12
|
threadId: string;
|
|
@@ -17,43 +15,3 @@ export interface ConversationTarget {
|
|
|
17
15
|
messageId?: string;
|
|
18
16
|
authenticity?: RemitImapMessageAuthenticity;
|
|
19
17
|
}
|
|
20
|
-
|
|
21
|
-
function threadToConversationTarget(
|
|
22
|
-
thread: RemitImapThreadMessageResponse,
|
|
23
|
-
): ConversationTarget {
|
|
24
|
-
return {
|
|
25
|
-
threadId: thread.threadId,
|
|
26
|
-
mailboxId: thread.mailboxId,
|
|
27
|
-
subject: thread.subject,
|
|
28
|
-
messageId: thread.messageId,
|
|
29
|
-
authenticity: thread.authenticity,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Resolve the conversation to open. Prefer the fully loaded thread (it carries
|
|
35
|
-
* the subject, authenticity and read state the reading pane wants); otherwise
|
|
36
|
-
* fall back to the `threadId` + `mailboxId` carried in the URL by a tapped
|
|
37
|
-
* "Related" hit so a message outside the loaded list still opens.
|
|
38
|
-
*/
|
|
39
|
-
export function buildConversationTarget(
|
|
40
|
-
selectedThread: RemitImapThreadMessageResponse | undefined,
|
|
41
|
-
fallback: {
|
|
42
|
-
messageId?: string;
|
|
43
|
-
threadId?: string;
|
|
44
|
-
mailboxId?: string;
|
|
45
|
-
},
|
|
46
|
-
): ConversationTarget | undefined {
|
|
47
|
-
if (selectedThread) return threadToConversationTarget(selectedThread);
|
|
48
|
-
// Require the messageId too: it's always set alongside the threadId when a
|
|
49
|
-
// result is tapped, and gating on it means clearing `selectedMessageId` (e.g.
|
|
50
|
-
// pressing Back) closes the conversation even if a stale threadId lingers.
|
|
51
|
-
if (fallback.messageId && fallback.threadId && fallback.mailboxId) {
|
|
52
|
-
return {
|
|
53
|
-
threadId: fallback.threadId,
|
|
54
|
-
mailboxId: fallback.mailboxId,
|
|
55
|
-
messageId: fallback.messageId,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
package/src/lib/mail-search.ts
CHANGED
|
@@ -16,7 +16,7 @@ const listSearch = z.object({ q: z.string().optional() });
|
|
|
16
16
|
* The brief's open thread and the message inside it are path segments
|
|
17
17
|
* (`/mail/brief/<thread>/<message>`), so the query carries nothing but the
|
|
18
18
|
* search. An old link's selection params are dropped here, which is what
|
|
19
|
-
* "tolerated and ignored" means until the
|
|
19
|
+
* "tolerated and ignored" means until the remaining lists follow.
|
|
20
20
|
*/
|
|
21
21
|
export const briefSearchSchema = listSearch.extend({});
|
|
22
22
|
|
|
@@ -31,13 +31,12 @@ export const flaggedSearchSchema = listSearch.extend({
|
|
|
31
31
|
*/
|
|
32
32
|
export const outboxSearchSchema = listSearch.extend({});
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
34
|
+
/**
|
|
35
|
+
* A folder's open thread and the message inside it are path segments
|
|
36
|
+
* (`/mail/<mailbox>/<thread>/<message>`), so the query carries nothing but the
|
|
37
|
+
* search.
|
|
38
|
+
*/
|
|
39
|
+
export const mailboxSearchSchema = listSearch.extend({});
|
|
41
40
|
|
|
42
41
|
/**
|
|
43
42
|
* `/mail` itself only redirects to the brief, so it accepts what the brief
|
package/src/routeTree.gen.ts
CHANGED
|
@@ -28,10 +28,13 @@ import { Route as SettingsLabelsRouteImport } from './routes/settings/labels'
|
|
|
28
28
|
import { Route as SettingsSendersRouteImport } from './routes/settings/senders'
|
|
29
29
|
import { Route as SettingsSuggestedVipsRouteImport } from './routes/settings/suggested-vips'
|
|
30
30
|
import { Route as MailMailboxIdIndexRouteImport } from './routes/mail/$mailboxId/index'
|
|
31
|
+
import { Route as MailMailboxIdThreadIdRouteImport } from './routes/mail/$mailboxId/$threadId'
|
|
31
32
|
import { Route as MailBriefIndexRouteImport } from './routes/mail/brief/index'
|
|
32
33
|
import { Route as MailBriefThreadIdRouteImport } from './routes/mail/brief/$threadId'
|
|
33
34
|
import { Route as MailFlaggedIndexRouteImport } from './routes/mail/flagged/index'
|
|
34
35
|
import { Route as MailOutboxIndexRouteImport } from './routes/mail/outbox/index'
|
|
36
|
+
import { Route as MailMailboxIdThreadIdIndexRouteImport } from './routes/mail/$mailboxId/$threadId/index'
|
|
37
|
+
import { Route as MailMailboxIdThreadIdMessageIdRouteImport } from './routes/mail/$mailboxId/$threadId/$messageId'
|
|
35
38
|
import { Route as MailBriefThreadIdIndexRouteImport } from './routes/mail/brief/$threadId/index'
|
|
36
39
|
import { Route as MailBriefThreadIdMessageIdRouteImport } from './routes/mail/brief/$threadId/$messageId'
|
|
37
40
|
import { Route as MailOutboxDraftOutboxMessageIdRouteImport } from './routes/mail/outbox/draft/$outboxMessageId'
|
|
@@ -131,6 +134,11 @@ const MailMailboxIdIndexRoute = MailMailboxIdIndexRouteImport.update({
|
|
|
131
134
|
path: '/',
|
|
132
135
|
getParentRoute: () => MailMailboxIdRoute,
|
|
133
136
|
} as any)
|
|
137
|
+
const MailMailboxIdThreadIdRoute = MailMailboxIdThreadIdRouteImport.update({
|
|
138
|
+
id: '/$threadId',
|
|
139
|
+
path: '/$threadId',
|
|
140
|
+
getParentRoute: () => MailMailboxIdRoute,
|
|
141
|
+
} as any)
|
|
134
142
|
const MailBriefIndexRoute = MailBriefIndexRouteImport.update({
|
|
135
143
|
id: '/',
|
|
136
144
|
path: '/',
|
|
@@ -151,6 +159,18 @@ const MailOutboxIndexRoute = MailOutboxIndexRouteImport.update({
|
|
|
151
159
|
path: '/',
|
|
152
160
|
getParentRoute: () => MailOutboxRoute,
|
|
153
161
|
} as any)
|
|
162
|
+
const MailMailboxIdThreadIdIndexRoute =
|
|
163
|
+
MailMailboxIdThreadIdIndexRouteImport.update({
|
|
164
|
+
id: '/',
|
|
165
|
+
path: '/',
|
|
166
|
+
getParentRoute: () => MailMailboxIdThreadIdRoute,
|
|
167
|
+
} as any)
|
|
168
|
+
const MailMailboxIdThreadIdMessageIdRoute =
|
|
169
|
+
MailMailboxIdThreadIdMessageIdRouteImport.update({
|
|
170
|
+
id: '/$messageId',
|
|
171
|
+
path: '/$messageId',
|
|
172
|
+
getParentRoute: () => MailMailboxIdThreadIdRoute,
|
|
173
|
+
} as any)
|
|
154
174
|
const MailBriefThreadIdIndexRoute = MailBriefThreadIdIndexRouteImport.update({
|
|
155
175
|
id: '/',
|
|
156
176
|
path: '/',
|
|
@@ -188,13 +208,16 @@ export interface FileRoutesByFullPath {
|
|
|
188
208
|
'/settings/suggested-vips': typeof SettingsSuggestedVipsRoute
|
|
189
209
|
'/mail/': typeof MailIndexRoute
|
|
190
210
|
'/settings/': typeof SettingsIndexRoute
|
|
211
|
+
'/mail/$mailboxId/$threadId': typeof MailMailboxIdThreadIdRouteWithChildren
|
|
191
212
|
'/mail/brief/$threadId': typeof MailBriefThreadIdRouteWithChildren
|
|
192
213
|
'/mail/$mailboxId/': typeof MailMailboxIdIndexRoute
|
|
193
214
|
'/mail/brief/': typeof MailBriefIndexRoute
|
|
194
215
|
'/mail/flagged/': typeof MailFlaggedIndexRoute
|
|
195
216
|
'/mail/outbox/': typeof MailOutboxIndexRoute
|
|
217
|
+
'/mail/$mailboxId/$threadId/$messageId': typeof MailMailboxIdThreadIdMessageIdRoute
|
|
196
218
|
'/mail/brief/$threadId/$messageId': typeof MailBriefThreadIdMessageIdRoute
|
|
197
219
|
'/mail/outbox/draft/$outboxMessageId': typeof MailOutboxDraftOutboxMessageIdRoute
|
|
220
|
+
'/mail/$mailboxId/$threadId/': typeof MailMailboxIdThreadIdIndexRoute
|
|
198
221
|
'/mail/brief/$threadId/': typeof MailBriefThreadIdIndexRoute
|
|
199
222
|
}
|
|
200
223
|
export interface FileRoutesByTo {
|
|
@@ -214,8 +237,10 @@ export interface FileRoutesByTo {
|
|
|
214
237
|
'/mail/brief': typeof MailBriefIndexRoute
|
|
215
238
|
'/mail/flagged': typeof MailFlaggedIndexRoute
|
|
216
239
|
'/mail/outbox': typeof MailOutboxIndexRoute
|
|
240
|
+
'/mail/$mailboxId/$threadId/$messageId': typeof MailMailboxIdThreadIdMessageIdRoute
|
|
217
241
|
'/mail/brief/$threadId/$messageId': typeof MailBriefThreadIdMessageIdRoute
|
|
218
242
|
'/mail/outbox/draft/$outboxMessageId': typeof MailOutboxDraftOutboxMessageIdRoute
|
|
243
|
+
'/mail/$mailboxId/$threadId': typeof MailMailboxIdThreadIdIndexRoute
|
|
219
244
|
'/mail/brief/$threadId': typeof MailBriefThreadIdIndexRoute
|
|
220
245
|
}
|
|
221
246
|
export interface FileRoutesById {
|
|
@@ -238,13 +263,16 @@ export interface FileRoutesById {
|
|
|
238
263
|
'/settings/suggested-vips': typeof SettingsSuggestedVipsRoute
|
|
239
264
|
'/mail/': typeof MailIndexRoute
|
|
240
265
|
'/settings/': typeof SettingsIndexRoute
|
|
266
|
+
'/mail/$mailboxId/$threadId': typeof MailMailboxIdThreadIdRouteWithChildren
|
|
241
267
|
'/mail/brief/$threadId': typeof MailBriefThreadIdRouteWithChildren
|
|
242
268
|
'/mail/$mailboxId/': typeof MailMailboxIdIndexRoute
|
|
243
269
|
'/mail/brief/': typeof MailBriefIndexRoute
|
|
244
270
|
'/mail/flagged/': typeof MailFlaggedIndexRoute
|
|
245
271
|
'/mail/outbox/': typeof MailOutboxIndexRoute
|
|
272
|
+
'/mail/$mailboxId/$threadId/$messageId': typeof MailMailboxIdThreadIdMessageIdRoute
|
|
246
273
|
'/mail/brief/$threadId/$messageId': typeof MailBriefThreadIdMessageIdRoute
|
|
247
274
|
'/mail/outbox/draft/$outboxMessageId': typeof MailOutboxDraftOutboxMessageIdRoute
|
|
275
|
+
'/mail/$mailboxId/$threadId/': typeof MailMailboxIdThreadIdIndexRoute
|
|
248
276
|
'/mail/brief/$threadId/': typeof MailBriefThreadIdIndexRoute
|
|
249
277
|
}
|
|
250
278
|
export interface FileRouteTypes {
|
|
@@ -268,13 +296,16 @@ export interface FileRouteTypes {
|
|
|
268
296
|
| '/settings/suggested-vips'
|
|
269
297
|
| '/mail/'
|
|
270
298
|
| '/settings/'
|
|
299
|
+
| '/mail/$mailboxId/$threadId'
|
|
271
300
|
| '/mail/brief/$threadId'
|
|
272
301
|
| '/mail/$mailboxId/'
|
|
273
302
|
| '/mail/brief/'
|
|
274
303
|
| '/mail/flagged/'
|
|
275
304
|
| '/mail/outbox/'
|
|
305
|
+
| '/mail/$mailboxId/$threadId/$messageId'
|
|
276
306
|
| '/mail/brief/$threadId/$messageId'
|
|
277
307
|
| '/mail/outbox/draft/$outboxMessageId'
|
|
308
|
+
| '/mail/$mailboxId/$threadId/'
|
|
278
309
|
| '/mail/brief/$threadId/'
|
|
279
310
|
fileRoutesByTo: FileRoutesByTo
|
|
280
311
|
to:
|
|
@@ -294,8 +325,10 @@ export interface FileRouteTypes {
|
|
|
294
325
|
| '/mail/brief'
|
|
295
326
|
| '/mail/flagged'
|
|
296
327
|
| '/mail/outbox'
|
|
328
|
+
| '/mail/$mailboxId/$threadId/$messageId'
|
|
297
329
|
| '/mail/brief/$threadId/$messageId'
|
|
298
330
|
| '/mail/outbox/draft/$outboxMessageId'
|
|
331
|
+
| '/mail/$mailboxId/$threadId'
|
|
299
332
|
| '/mail/brief/$threadId'
|
|
300
333
|
id:
|
|
301
334
|
| '__root__'
|
|
@@ -317,13 +350,16 @@ export interface FileRouteTypes {
|
|
|
317
350
|
| '/settings/suggested-vips'
|
|
318
351
|
| '/mail/'
|
|
319
352
|
| '/settings/'
|
|
353
|
+
| '/mail/$mailboxId/$threadId'
|
|
320
354
|
| '/mail/brief/$threadId'
|
|
321
355
|
| '/mail/$mailboxId/'
|
|
322
356
|
| '/mail/brief/'
|
|
323
357
|
| '/mail/flagged/'
|
|
324
358
|
| '/mail/outbox/'
|
|
359
|
+
| '/mail/$mailboxId/$threadId/$messageId'
|
|
325
360
|
| '/mail/brief/$threadId/$messageId'
|
|
326
361
|
| '/mail/outbox/draft/$outboxMessageId'
|
|
362
|
+
| '/mail/$mailboxId/$threadId/'
|
|
327
363
|
| '/mail/brief/$threadId/'
|
|
328
364
|
fileRoutesById: FileRoutesById
|
|
329
365
|
}
|
|
@@ -469,6 +505,13 @@ declare module '@tanstack/react-router' {
|
|
|
469
505
|
preLoaderRoute: typeof MailMailboxIdIndexRouteImport
|
|
470
506
|
parentRoute: typeof MailMailboxIdRoute
|
|
471
507
|
}
|
|
508
|
+
'/mail/$mailboxId/$threadId': {
|
|
509
|
+
id: '/mail/$mailboxId/$threadId'
|
|
510
|
+
path: '/$threadId'
|
|
511
|
+
fullPath: '/mail/$mailboxId/$threadId'
|
|
512
|
+
preLoaderRoute: typeof MailMailboxIdThreadIdRouteImport
|
|
513
|
+
parentRoute: typeof MailMailboxIdRoute
|
|
514
|
+
}
|
|
472
515
|
'/mail/brief/': {
|
|
473
516
|
id: '/mail/brief/'
|
|
474
517
|
path: '/'
|
|
@@ -497,6 +540,20 @@ declare module '@tanstack/react-router' {
|
|
|
497
540
|
preLoaderRoute: typeof MailOutboxIndexRouteImport
|
|
498
541
|
parentRoute: typeof MailOutboxRoute
|
|
499
542
|
}
|
|
543
|
+
'/mail/$mailboxId/$threadId/': {
|
|
544
|
+
id: '/mail/$mailboxId/$threadId/'
|
|
545
|
+
path: '/'
|
|
546
|
+
fullPath: '/mail/$mailboxId/$threadId/'
|
|
547
|
+
preLoaderRoute: typeof MailMailboxIdThreadIdIndexRouteImport
|
|
548
|
+
parentRoute: typeof MailMailboxIdThreadIdRoute
|
|
549
|
+
}
|
|
550
|
+
'/mail/$mailboxId/$threadId/$messageId': {
|
|
551
|
+
id: '/mail/$mailboxId/$threadId/$messageId'
|
|
552
|
+
path: '/$messageId'
|
|
553
|
+
fullPath: '/mail/$mailboxId/$threadId/$messageId'
|
|
554
|
+
preLoaderRoute: typeof MailMailboxIdThreadIdMessageIdRouteImport
|
|
555
|
+
parentRoute: typeof MailMailboxIdThreadIdRoute
|
|
556
|
+
}
|
|
500
557
|
'/mail/brief/$threadId/': {
|
|
501
558
|
id: '/mail/brief/$threadId/'
|
|
502
559
|
path: '/'
|
|
@@ -521,11 +578,28 @@ declare module '@tanstack/react-router' {
|
|
|
521
578
|
}
|
|
522
579
|
}
|
|
523
580
|
|
|
581
|
+
interface MailMailboxIdThreadIdRouteChildren {
|
|
582
|
+
MailMailboxIdThreadIdMessageIdRoute: typeof MailMailboxIdThreadIdMessageIdRoute
|
|
583
|
+
MailMailboxIdThreadIdIndexRoute: typeof MailMailboxIdThreadIdIndexRoute
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const MailMailboxIdThreadIdRouteChildren: MailMailboxIdThreadIdRouteChildren = {
|
|
587
|
+
MailMailboxIdThreadIdMessageIdRoute: MailMailboxIdThreadIdMessageIdRoute,
|
|
588
|
+
MailMailboxIdThreadIdIndexRoute: MailMailboxIdThreadIdIndexRoute,
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const MailMailboxIdThreadIdRouteWithChildren =
|
|
592
|
+
MailMailboxIdThreadIdRoute._addFileChildren(
|
|
593
|
+
MailMailboxIdThreadIdRouteChildren,
|
|
594
|
+
)
|
|
595
|
+
|
|
524
596
|
interface MailMailboxIdRouteChildren {
|
|
597
|
+
MailMailboxIdThreadIdRoute: typeof MailMailboxIdThreadIdRouteWithChildren
|
|
525
598
|
MailMailboxIdIndexRoute: typeof MailMailboxIdIndexRoute
|
|
526
599
|
}
|
|
527
600
|
|
|
528
601
|
const MailMailboxIdRouteChildren: MailMailboxIdRouteChildren = {
|
|
602
|
+
MailMailboxIdThreadIdRoute: MailMailboxIdThreadIdRouteWithChildren,
|
|
529
603
|
MailMailboxIdIndexRoute: MailMailboxIdIndexRoute,
|
|
530
604
|
}
|
|
531
605
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /mail/$mailboxId/$threadId/$messageId — the same conversation, with one of its
|
|
3
|
+
* messages expanded and scrolled to.
|
|
4
|
+
*
|
|
5
|
+
* A message is not addressable on its own: `GET /messages/{messageId}` answers
|
|
6
|
+
* with no thread, so the pane has nothing to fetch by. The segment names which
|
|
7
|
+
* message inside the thread the reader pointed at, and the surface it renders is
|
|
8
|
+
* the thread's.
|
|
9
|
+
*/
|
|
10
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
11
|
+
import { MailboxPane } from "@/components/mail/MailboxPane";
|
|
12
|
+
|
|
13
|
+
function MailboxMessagePane() {
|
|
14
|
+
return <MailboxPane.Reading />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Route = createFileRoute("/mail/$mailboxId/$threadId/$messageId")({
|
|
18
|
+
component: MailboxMessagePane,
|
|
19
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thread with no message named. The pane opens on the newest message, the
|
|
3
|
+
* same as for a thread whose row the reader never pointed at.
|
|
4
|
+
*/
|
|
5
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
6
|
+
import { MailboxPane } from "@/components/mail/MailboxPane";
|
|
7
|
+
|
|
8
|
+
function MailboxThreadPane() {
|
|
9
|
+
return <MailboxPane.Reading />;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Route = createFileRoute("/mail/$mailboxId/$threadId/")({
|
|
13
|
+
component: MailboxThreadPane,
|
|
14
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /mail/$mailboxId/$threadId — a conversation open in a folder's reading pane.
|
|
3
|
+
*
|
|
4
|
+
* The folder in the address is the list being browsed, not the thread's home:
|
|
5
|
+
* a cross-folder search hit opens here too, and every message it holds names
|
|
6
|
+
* its own mailbox. A layout, because the message segment nests under it.
|
|
7
|
+
*/
|
|
8
|
+
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
|
9
|
+
|
|
10
|
+
export const Route = createFileRoute("/mail/$mailboxId/$threadId")({
|
|
11
|
+
component: Outlet,
|
|
12
|
+
});
|
|
@@ -4,26 +4,30 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The literal lists are declared as siblings and TanStack matches literals
|
|
6
6
|
* first, so a mailbox id can never be read as one of them.
|
|
7
|
+
*
|
|
8
|
+
* The open thread and the message inside it are the segments below, which is why
|
|
9
|
+
* nothing here reads a selection out of the query.
|
|
7
10
|
*/
|
|
8
11
|
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
|
9
12
|
import { MailShell } from "@/components/layout/MailShell";
|
|
10
13
|
import { MailboxPane } from "@/components/mail/MailboxPane";
|
|
11
14
|
import { useSearchMirror } from "@/hooks/useSearchMirror";
|
|
12
15
|
import { mailboxSearchSchema } from "@/lib/mail-search";
|
|
16
|
+
import { useOpenThreadPath } from "@/routing";
|
|
13
17
|
|
|
14
18
|
function MailboxLayout() {
|
|
15
19
|
const { mailboxId } = Route.useParams();
|
|
16
|
-
const
|
|
20
|
+
const thread = useOpenThreadPath();
|
|
17
21
|
useSearchMirror({ to: "/mail/$mailboxId", params: { mailboxId } });
|
|
18
22
|
|
|
19
23
|
return (
|
|
20
|
-
<MailboxPane mailboxId={mailboxId}
|
|
24
|
+
<MailboxPane mailboxId={mailboxId} thread={thread}>
|
|
21
25
|
<MailShell
|
|
22
26
|
phone={<MailboxPane.Phone />}
|
|
23
27
|
list={<MailboxPane.List />}
|
|
24
28
|
reading={<Outlet />}
|
|
25
29
|
intelligence={<MailboxPane.Intelligence />}
|
|
26
|
-
hasThread={Boolean(
|
|
30
|
+
hasThread={Boolean(thread)}
|
|
27
31
|
/>
|
|
28
32
|
</MailboxPane>
|
|
29
33
|
);
|
|
@@ -17,7 +17,7 @@ import { BriefPane } from "@/components/mail/BriefPane";
|
|
|
17
17
|
import { ErrorState } from "@/components/ui/ErrorState";
|
|
18
18
|
import { useSearchMirror } from "@/hooks/useSearchMirror";
|
|
19
19
|
import { briefSearchSchema } from "@/lib/mail-search";
|
|
20
|
-
import {
|
|
20
|
+
import { useOpenThreadPath } from "@/routing";
|
|
21
21
|
|
|
22
22
|
const BriefError = ({ error, reset }: ErrorComponentProps) => (
|
|
23
23
|
<div className="flex h-full items-center justify-center bg-canvas p-4">
|
|
@@ -30,7 +30,7 @@ const BriefError = ({ error, reset }: ErrorComponentProps) => (
|
|
|
30
30
|
);
|
|
31
31
|
|
|
32
32
|
function BriefLayout() {
|
|
33
|
-
const thread =
|
|
33
|
+
const thread = useOpenThreadPath();
|
|
34
34
|
useSearchMirror({ to: "/mail/brief" });
|
|
35
35
|
|
|
36
36
|
return (
|
package/src/routing/index.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
export {
|
|
2
|
-
type BriefThreadPath,
|
|
3
|
-
type BriefThreadTarget,
|
|
4
|
-
useBriefThreadPath,
|
|
5
|
-
} from "./brief-thread";
|
|
6
1
|
export {
|
|
7
2
|
type PanelFragment,
|
|
8
3
|
panelFragments,
|
|
@@ -10,4 +5,9 @@ export {
|
|
|
10
5
|
useOpenPanel,
|
|
11
6
|
} from "./fragment";
|
|
12
7
|
export { NavLink, type NavLinkProps } from "./nav-link";
|
|
8
|
+
export {
|
|
9
|
+
type OpenThreadPath,
|
|
10
|
+
type OpenThreadTarget,
|
|
11
|
+
useOpenThreadPath,
|
|
12
|
+
} from "./open-thread";
|
|
13
13
|
export { useOutboxDraftId } from "./outbox-draft";
|