@remit/web-client 0.0.149 → 0.0.151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +4 -3
- 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/OutboxPane.tsx +31 -28
- 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/lib/conversation-target.ts +7 -49
- package/src/lib/mail-search.ts +13 -11
- package/src/routeTree.gen.ts +96 -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/routes/mail/outbox/draft/$outboxMessageId.tsx +19 -0
- package/src/routes/mail/outbox.tsx +6 -1
- package/src/routing/index.ts +6 -5
- package/src/routing/open-thread.ts +58 -0
- package/src/routing/outbox-draft.ts +17 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.151",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
|
|
6
6
|
"exports": {
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
RemitImapDescribeMessageResponse,
|
|
9
9
|
} from "@remit/api-http-client/types.gen.ts";
|
|
10
10
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
11
|
-
import { useLocation, useNavigate } from "@tanstack/react-router";
|
|
11
|
+
import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
|
|
12
12
|
import {
|
|
13
13
|
createContext,
|
|
14
14
|
useCallback,
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { useErrorBanners } from "@/components/ui/ErrorBannerProvider";
|
|
22
22
|
import { useComposeTarget } from "@/hooks/useComposeTargetMailbox";
|
|
23
23
|
import { hostsComposeSurface } from "@/lib/compose-routes";
|
|
24
|
+
import { locationOpensDetail } from "@/lib/mail-route";
|
|
24
25
|
export type ComposeMode = "reply" | "reply_all" | "forward" | "new";
|
|
25
26
|
|
|
26
27
|
export interface ComposeState {
|
|
@@ -66,6 +67,7 @@ export const ComposeProvider = ({
|
|
|
66
67
|
const queryClient = useQueryClient();
|
|
67
68
|
const navigate = useNavigate();
|
|
68
69
|
const location = useLocation();
|
|
70
|
+
const routeParams = useParams({ strict: false, shouldThrow: false });
|
|
69
71
|
const { pushError } = useErrorBanners();
|
|
70
72
|
// Compose is a mail action, so its target only has to be known under `/mail`.
|
|
71
73
|
// Resolving it from the root otherwise costs `/settings` and `/onboarding` a
|
|
@@ -114,9 +116,14 @@ export const ComposeProvider = ({
|
|
|
114
116
|
const openCompose = useCallback(
|
|
115
117
|
(params: Omit<ComposeState, "isOpen">) => {
|
|
116
118
|
const search = location.search as Record<string, unknown>;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
119
|
+
// A folder names its open conversation in the path, so leaving it is a
|
|
120
|
+
// navigation up to the list; the lists still to move name it in the query.
|
|
121
|
+
const threadMailboxId = locationOpensDetail(location.pathname)
|
|
122
|
+
? routeParams?.mailboxId
|
|
123
|
+
: undefined;
|
|
124
|
+
const showsThread =
|
|
125
|
+
Boolean(threadMailboxId) ||
|
|
126
|
+
Boolean(search.selectedMessageId ?? search.selectedThreadId);
|
|
120
127
|
if (!hostsComposeSurface(location.pathname)) {
|
|
121
128
|
if (target.status === "loading") {
|
|
122
129
|
pushError({
|
|
@@ -145,6 +152,14 @@ export const ComposeProvider = ({
|
|
|
145
152
|
setState({ ...params, isOpen: true });
|
|
146
153
|
if (!showsThread) return;
|
|
147
154
|
// A push, so Back reopens the message.
|
|
155
|
+
if (threadMailboxId) {
|
|
156
|
+
navigate({
|
|
157
|
+
to: "/mail/$mailboxId",
|
|
158
|
+
params: { mailboxId: threadMailboxId },
|
|
159
|
+
search: (prev) => prev,
|
|
160
|
+
});
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
148
163
|
navigate({
|
|
149
164
|
to: ".",
|
|
150
165
|
search: (prev: Record<string, unknown>) => ({
|
|
@@ -154,7 +169,14 @@ export const ComposeProvider = ({
|
|
|
154
169
|
}),
|
|
155
170
|
});
|
|
156
171
|
},
|
|
157
|
-
[
|
|
172
|
+
[
|
|
173
|
+
navigate,
|
|
174
|
+
location.pathname,
|
|
175
|
+
location.search,
|
|
176
|
+
routeParams?.mailboxId,
|
|
177
|
+
target,
|
|
178
|
+
pushError,
|
|
179
|
+
],
|
|
158
180
|
);
|
|
159
181
|
|
|
160
182
|
const closeCompose = useCallback(() => {
|
|
@@ -14,8 +14,9 @@ import { locationOpensDetail } from "@/lib/mail-route";
|
|
|
14
14
|
* `lg:hidden` class covers the pre-hydration frame.
|
|
15
15
|
* - The compose surface is already open.
|
|
16
16
|
* - The user is reading a thread — the single pane is the conversation, and
|
|
17
|
-
* its reply bar is under this corner. The brief
|
|
18
|
-
*
|
|
17
|
+
* its reply bar is under this corner. The brief and a folder say so in
|
|
18
|
+
* their path; the flagged list, still to move, says so in
|
|
19
|
+
* `?selectedMessageId=…`.
|
|
19
20
|
* - The user is off `/mail`, which is every route with no mail in it.
|
|
20
21
|
*/
|
|
21
22
|
export const ComposeFab = () => {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Usage in the list layout route:
|
|
5
5
|
*
|
|
6
|
-
* <BriefPane thread={
|
|
6
|
+
* <BriefPane thread={useOpenThreadPath()}>
|
|
7
7
|
* <MailShell
|
|
8
8
|
* list={<BriefPane.List />}
|
|
9
9
|
* reading={<Outlet />}
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from "@/hooks/useTriageLayer";
|
|
42
42
|
import type { ConversationTarget } from "@/lib/conversation-target";
|
|
43
43
|
import { useMailContext } from "@/lib/mail-context";
|
|
44
|
-
import type {
|
|
44
|
+
import type { OpenThreadPath, OpenThreadTarget } from "@/routing";
|
|
45
45
|
|
|
46
46
|
/* ------------------------------------------------------------------ */
|
|
47
47
|
/* Context */
|
|
@@ -54,13 +54,13 @@ interface BriefPaneContextValue {
|
|
|
54
54
|
/** The conversation the pane shows, or none when the address names no thread. */
|
|
55
55
|
conversation: ConversationTarget | undefined;
|
|
56
56
|
onOpenThread: (
|
|
57
|
-
target:
|
|
57
|
+
target: OpenThreadTarget,
|
|
58
58
|
options?: OpenMessageOptions,
|
|
59
59
|
) => void;
|
|
60
60
|
onCloseThread: () => void;
|
|
61
61
|
/** The rows either side of the open one — the phone's swipe gestures. */
|
|
62
|
-
nextThread:
|
|
63
|
-
previousThread:
|
|
62
|
+
nextThread: OpenThreadTarget | undefined;
|
|
63
|
+
previousThread: OpenThreadTarget | undefined;
|
|
64
64
|
/**
|
|
65
65
|
* Toolbar verbs for the open thread, keyed by the thread's own mailbox and
|
|
66
66
|
* account — the brief spans accounts, so there is no route mailbox to key by.
|
|
@@ -92,7 +92,7 @@ function useBriefPane(): BriefPaneContextValue {
|
|
|
92
92
|
|
|
93
93
|
interface BriefPaneProps {
|
|
94
94
|
/** The open conversation, as the address states it. */
|
|
95
|
-
thread:
|
|
95
|
+
thread: OpenThreadPath | undefined;
|
|
96
96
|
children: ReactNode;
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -137,7 +137,7 @@ function BriefPaneProvider({ thread, children }: BriefPaneProps) {
|
|
|
137
137
|
}, [threadId, selectedThread, selectedMessageId]);
|
|
138
138
|
|
|
139
139
|
const handleOpenThread = useCallback(
|
|
140
|
-
(target:
|
|
140
|
+
(target: OpenThreadTarget, options?: OpenMessageOptions) => {
|
|
141
141
|
navigate({
|
|
142
142
|
to: "/mail/brief/$threadId/$messageId",
|
|
143
143
|
params: target,
|
|
@@ -225,7 +225,7 @@ function BriefPaneProvider({ thread, children }: BriefPaneProps) {
|
|
|
225
225
|
// name its thread. The brief's own listing is where that is looked up, so a
|
|
226
226
|
// row it does not hold offers no gesture rather than a tap that goes nowhere.
|
|
227
227
|
const adjacentThread = useCallback(
|
|
228
|
-
(messageId: string | undefined):
|
|
228
|
+
(messageId: string | undefined): OpenThreadTarget | undefined => {
|
|
229
229
|
if (!messageId) return undefined;
|
|
230
230
|
const row = briefThreads.find((t) => t.messageId === messageId);
|
|
231
231
|
return row ? { threadId: row.threadId, messageId } : undefined;
|
|
@@ -105,7 +105,7 @@ import {
|
|
|
105
105
|
type SelectionWizardControl,
|
|
106
106
|
useSelectionWizard,
|
|
107
107
|
} from "@/lib/wizard-history";
|
|
108
|
-
import type {
|
|
108
|
+
import type { OpenThreadTarget } from "@/routing";
|
|
109
109
|
import { LabelApplyTrigger } from "./LabelApplyTrigger";
|
|
110
110
|
import { MailListHeader, type MailListHeaderProps } from "./MailListHeader";
|
|
111
111
|
import type { MessageListCommands } from "./MessageList";
|
|
@@ -385,7 +385,7 @@ interface DailyBriefProps {
|
|
|
385
385
|
* from the unified inbox.
|
|
386
386
|
*/
|
|
387
387
|
onOpenThread?: (
|
|
388
|
-
target:
|
|
388
|
+
target: OpenThreadTarget,
|
|
389
389
|
options?: OpenMessageOptions,
|
|
390
390
|
) => void;
|
|
391
391
|
/** Where the list publishes the commands the keyboard layer drives. */
|
|
@@ -825,11 +825,7 @@ export function DailyBrief({
|
|
|
825
825
|
navigate({
|
|
826
826
|
to: "/mail/$mailboxId",
|
|
827
827
|
params: { mailboxId: briefSpamOffer.mailboxId },
|
|
828
|
-
search: {
|
|
829
|
-
q: searchQuery || undefined,
|
|
830
|
-
selectedMessageId: undefined,
|
|
831
|
-
selectedThreadId: undefined,
|
|
832
|
-
},
|
|
828
|
+
search: { q: searchQuery || undefined },
|
|
833
829
|
})
|
|
834
830
|
}
|
|
835
831
|
/>
|
|
@@ -189,10 +189,14 @@ export function DraftsView({
|
|
|
189
189
|
};
|
|
190
190
|
|
|
191
191
|
const handleImapDraftOpen = (messageId: string) => {
|
|
192
|
+
const threadId = imapThreads.find(
|
|
193
|
+
(thread) => thread.messageId === messageId,
|
|
194
|
+
)?.threadId;
|
|
195
|
+
if (!threadId) return;
|
|
192
196
|
navigate({
|
|
193
|
-
to: "/mail/$mailboxId",
|
|
194
|
-
params: { mailboxId },
|
|
195
|
-
search:
|
|
197
|
+
to: "/mail/$mailboxId/$threadId/$messageId",
|
|
198
|
+
params: { mailboxId, threadId, messageId },
|
|
199
|
+
search: (prev) => prev,
|
|
196
200
|
});
|
|
197
201
|
};
|
|
198
202
|
|
|
@@ -310,15 +310,16 @@ function WiredPanel({
|
|
|
310
310
|
// preserving deep-linking, middle-click, and existing search params.
|
|
311
311
|
const similarLinkComponent: SimilarMessageLinkComponent = ({
|
|
312
312
|
mailboxId: rowMailboxId,
|
|
313
|
+
threadId,
|
|
313
314
|
messageId,
|
|
314
315
|
className,
|
|
315
316
|
ariaLabel,
|
|
316
317
|
children,
|
|
317
318
|
}) => (
|
|
318
319
|
<NavLink
|
|
319
|
-
to="/mail/$mailboxId"
|
|
320
|
-
params={{ mailboxId: rowMailboxId }}
|
|
321
|
-
search={(prev) =>
|
|
320
|
+
to="/mail/$mailboxId/$threadId/$messageId"
|
|
321
|
+
params={{ mailboxId: rowMailboxId, threadId, messageId }}
|
|
322
|
+
search={(prev) => prev}
|
|
322
323
|
variant="row"
|
|
323
324
|
className={className}
|
|
324
325
|
aria-label={ariaLabel}
|
|
@@ -323,11 +323,7 @@ export function MailListHeader({
|
|
|
323
323
|
navigate({
|
|
324
324
|
to: "/mail/$mailboxId",
|
|
325
325
|
params: { mailboxId: spamOffer.mailboxId },
|
|
326
|
-
search: {
|
|
327
|
-
q: searchQuery || undefined,
|
|
328
|
-
selectedMessageId: undefined,
|
|
329
|
-
selectedThreadId: undefined,
|
|
330
|
-
},
|
|
326
|
+
search: { q: searchQuery || undefined },
|
|
331
327
|
}),
|
|
332
328
|
}
|
|
333
329
|
: routeScope;
|
|
@@ -244,7 +244,7 @@ export function MailSidebarAdapter({
|
|
|
244
244
|
return (
|
|
245
245
|
<NavLink
|
|
246
246
|
to="/mail/outbox"
|
|
247
|
-
search={{ q: undefined
|
|
247
|
+
search={{ q: undefined }}
|
|
248
248
|
onClick={() => onClick?.()}
|
|
249
249
|
className={className}
|
|
250
250
|
aria-label={ariaLabel}
|
|
@@ -285,8 +285,9 @@ export function MailSidebarAdapter({
|
|
|
285
285
|
<NavLink
|
|
286
286
|
to="/mail/$mailboxId"
|
|
287
287
|
params={{ mailboxId: navId }}
|
|
288
|
-
// Drop any stale search query
|
|
289
|
-
|
|
288
|
+
// Drop any stale search query when switching mailbox; the list route
|
|
289
|
+
// is above the open conversation, so it leaves nothing behind.
|
|
290
|
+
search={{ q: undefined }}
|
|
290
291
|
onClick={() => {
|
|
291
292
|
invalidateMailboxThreads(navId);
|
|
292
293
|
onClick?.();
|