@remit/web-client 0.0.166 → 0.0.168
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 +91 -29
- package/src/components/compose/ComposeProvider.tsx +19 -124
- package/src/components/compose/ConversationCompose.tsx +57 -0
- package/src/components/compose/FullCompose.tsx +29 -18
- 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 +49 -13
- package/src/components/mail/ConversationView.tsx +82 -77
- package/src/components/mail/DraftsView.tsx +12 -14
- package/src/components/mail/FlaggedPane.tsx +49 -13
- package/src/components/mail/MailboxPane.tsx +77 -167
- package/src/components/mail/OutboxPane.tsx +3 -9
- package/src/components/mail/conversation-reply-reach.render.test.ts +81 -28
- package/src/hooks/useSaveDraft.ts +11 -0
- package/src/hooks/useSearchMirror.ts +12 -1
- package/src/hooks/useThreadActions.ts +8 -17
- package/src/lib/mail-route.test.ts +35 -1
- package/src/lib/mail-route.ts +2 -1
- package/src/routeTree.gen.ts +220 -15
- package/src/routes/mail/$mailboxId/$threadId/$messageId/$mode.{-$outboxMessageId}.tsx +14 -0
- package/src/routes/mail/$mailboxId/$threadId/$messageId.tsx +4 -0
- package/src/routes/mail/$mailboxId/compose.{-$outboxMessageId}.tsx +14 -0
- package/src/routes/mail/brief/$threadId/$messageId/$mode.{-$outboxMessageId}.tsx +26 -0
- package/src/routes/mail/brief/$threadId/$messageId.tsx +4 -0
- package/src/routes/mail/brief/compose.{-$outboxMessageId}.tsx +22 -0
- package/src/routes/mail/flagged/$threadId/$messageId/$mode.{-$outboxMessageId}.tsx +14 -0
- package/src/routes/mail/flagged/$threadId/$messageId.tsx +4 -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/browsed-list.ts +22 -0
- package/src/routing/compose-press-opens.render.test.ts +179 -0
- package/src/routing/compose.ts +205 -0
- package/src/routing/index.ts +20 -0
- package/src/routing/reply.ts +282 -0
- package/src/components/compose/InlineCompose.tsx +0 -37
- 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
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
configOperationsGetConfigOptions,
|
|
3
|
-
messageOperationsDescribeMessageOptions,
|
|
4
3
|
threadDetailOperationsListThreadMessagesOptions,
|
|
5
4
|
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
6
5
|
import type { RemitImapMessageAuthenticity } from "@remit/api-http-client/types.gen.ts";
|
|
7
6
|
import { MobileReadingPane } from "@remit/ui";
|
|
8
7
|
import { useQuery } from "@tanstack/react-query";
|
|
9
8
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
10
|
-
import
|
|
11
|
-
import { InlineCompose } from "@/components/compose/InlineCompose";
|
|
9
|
+
import { ConversationCompose } from "@/components/compose/ConversationCompose";
|
|
12
10
|
import { EmptyState } from "@/components/ui/EmptyState";
|
|
13
11
|
import { ErrorState } from "@/components/ui/ErrorState";
|
|
14
12
|
import { useKeyboardNavigation } from "@/hooks/useKeyboardNavigation";
|
|
@@ -17,6 +15,7 @@ import { useMarkAsRead } from "@/hooks/useMarkAsRead";
|
|
|
17
15
|
import { useIsDesktop } from "@/hooks/useMediaQuery";
|
|
18
16
|
import { useSwipeNavigation } from "@/hooks/useSwipeNavigation";
|
|
19
17
|
import { useToggleStar } from "@/hooks/useToggleStar";
|
|
18
|
+
import { type ReplyMode, useOpenReply, useReplySurface } from "@/routing";
|
|
20
19
|
import { AuthenticityBanner } from "./AuthenticityBanner";
|
|
21
20
|
import { MessageCard } from "./MessageCard";
|
|
22
21
|
|
|
@@ -47,21 +46,6 @@ interface ConversationViewProps {
|
|
|
47
46
|
* list is always visible in the resizable side pane.
|
|
48
47
|
*/
|
|
49
48
|
onBack?: () => void;
|
|
50
|
-
/**
|
|
51
|
-
* When set, immediately opens the inline compose in this mode. The
|
|
52
|
-
* parent (e.g. the reading-pane toolbar) sets this to wire the
|
|
53
|
-
* top-bar reply/reply-all/forward buttons into the inline compose
|
|
54
|
-
* that lives inside the conversation. Reset by calling
|
|
55
|
-
* `onComposeClose` after the compose opens.
|
|
56
|
-
*/
|
|
57
|
-
composeRequest?: ComposeMode | null;
|
|
58
|
-
/**
|
|
59
|
-
* Called once the conversation has acted on `composeRequest` (or
|
|
60
|
-
* whenever the inline compose is dismissed). The caller must reset
|
|
61
|
-
* `composeRequest` to `null`: a request left standing is read as a fresh
|
|
62
|
-
* one on every render.
|
|
63
|
-
*/
|
|
64
|
-
onComposeClose?: () => void;
|
|
65
49
|
/**
|
|
66
50
|
* Mobile only: swipe-left handler to open the next message in the list the
|
|
67
51
|
* thread was opened from. Omitted at the end of the list so the gesture
|
|
@@ -103,8 +87,6 @@ export const ConversationView = ({
|
|
|
103
87
|
authenticity,
|
|
104
88
|
onOpenIntelligence,
|
|
105
89
|
onBack,
|
|
106
|
-
composeRequest,
|
|
107
|
-
onComposeClose,
|
|
108
90
|
onSwipeNext,
|
|
109
91
|
onSwipePrevious,
|
|
110
92
|
mobileIntelligenceOpen,
|
|
@@ -240,72 +222,65 @@ export const ConversationView = ({
|
|
|
240
222
|
messages,
|
|
241
223
|
});
|
|
242
224
|
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// Notify the parent that the request has been consumed.
|
|
262
|
-
onComposeClose?.();
|
|
263
|
-
}
|
|
264
|
-
}, [composeRequest, onComposeClose, openCompose]);
|
|
265
|
-
|
|
266
|
-
// Reply and forward act on the latest turn of the conversation, which is the
|
|
267
|
-
// last message now that the thread reads oldest first.
|
|
268
|
-
const { data: latestMessageData } = useQuery({
|
|
269
|
-
...messageOperationsDescribeMessageOptions({
|
|
270
|
-
path: { messageId: latestMessage?.messageId ?? "" },
|
|
271
|
-
}),
|
|
272
|
-
enabled: !!latestMessage && composeMode !== null,
|
|
273
|
-
});
|
|
225
|
+
// The reply is a segment under the message it answers, so what is being
|
|
226
|
+
// written and which turn it answers are the address's to state. Nothing here
|
|
227
|
+
// keeps a second opinion about whether a composer is up.
|
|
228
|
+
const reply = useReplySurface();
|
|
229
|
+
const openReply = useOpenReply();
|
|
230
|
+
|
|
231
|
+
// Which turn a verb aimed at the whole conversation answers: the message the
|
|
232
|
+
// address names, or the latest turn when it names none. A per-message button
|
|
233
|
+
// passes its own instead, which the address could not express before this.
|
|
234
|
+
const answeredMessageId = selectedMessageId ?? latestMessage?.messageId;
|
|
235
|
+
|
|
236
|
+
const replyWith = useCallback(
|
|
237
|
+
(mode: ReplyMode, messageId: string | undefined) => {
|
|
238
|
+
if (!messageId) return;
|
|
239
|
+
openReply({ threadId, messageId, mode });
|
|
240
|
+
},
|
|
241
|
+
[openReply, threadId],
|
|
242
|
+
);
|
|
274
243
|
|
|
275
|
-
const handleReply = useCallback(
|
|
244
|
+
const handleReply = useCallback(
|
|
245
|
+
() => replyWith("reply", answeredMessageId),
|
|
246
|
+
[replyWith, answeredMessageId],
|
|
247
|
+
);
|
|
276
248
|
|
|
277
249
|
const handleReplyAll = useCallback(
|
|
278
|
-
() =>
|
|
279
|
-
[
|
|
250
|
+
() => replyWith("reply-all", answeredMessageId),
|
|
251
|
+
[replyWith, answeredMessageId],
|
|
280
252
|
);
|
|
281
253
|
|
|
282
254
|
const handleForward = useCallback(
|
|
283
|
-
() =>
|
|
284
|
-
[
|
|
255
|
+
() => replyWith("forward", answeredMessageId),
|
|
256
|
+
[replyWith, answeredMessageId],
|
|
285
257
|
);
|
|
286
258
|
|
|
287
|
-
const handleCloseCompose = useCallback(() => {
|
|
288
|
-
setComposeMode(null);
|
|
289
|
-
onComposeClose?.();
|
|
290
|
-
}, [onComposeClose]);
|
|
291
|
-
|
|
292
259
|
// Compose opens at the head of the pane, which from halfway down a long
|
|
293
260
|
// thread is several screens up: without this, replying looks like nothing
|
|
294
261
|
// happening. Aligned on its top edge, where the recipients are, and left
|
|
295
262
|
// alone afterwards — it grows downward as it is written into, so following
|
|
296
263
|
// its height would pull the rows being typed into off the top.
|
|
264
|
+
//
|
|
265
|
+
// Keyed on what is being answered and how, not on the draft under it: the
|
|
266
|
+
// address gains a draft segment while the reader types, and scrolling there
|
|
267
|
+
// would move the pane mid-sentence.
|
|
297
268
|
const composeRef = useRef<HTMLDivElement>(null);
|
|
269
|
+
const answering =
|
|
270
|
+
reply?.kind === "reply"
|
|
271
|
+
? `${reply.mode}:${reply.sourceMessageId}`
|
|
272
|
+
: undefined;
|
|
298
273
|
useEffect(() => {
|
|
299
|
-
if (
|
|
274
|
+
if (!answering) return;
|
|
300
275
|
composeRef.current?.scrollIntoView({
|
|
301
276
|
behavior: "smooth",
|
|
302
277
|
block: "start",
|
|
303
278
|
});
|
|
304
|
-
}, [
|
|
279
|
+
}, [answering]);
|
|
305
280
|
|
|
306
281
|
// Register keyboard shortcuts
|
|
307
282
|
useKeyboardNavigation({
|
|
308
|
-
enabled: !isLoading && messages.length > 0 &&
|
|
283
|
+
enabled: !isLoading && messages.length > 0 && reply === undefined,
|
|
309
284
|
bindings: [
|
|
310
285
|
{ key: "j", handler: focusNext, preventDefault: true },
|
|
311
286
|
{ key: "ArrowDown", handler: focusNext, preventDefault: true },
|
|
@@ -340,10 +315,22 @@ export const ConversationView = ({
|
|
|
340
315
|
);
|
|
341
316
|
}
|
|
342
317
|
|
|
318
|
+
// An address naming a reply over a conversation with nothing in it says so.
|
|
319
|
+
// The mailbox is shared with other clients, so a thread can empty underneath
|
|
320
|
+
// the reader between opening it and answering it, and a composer that simply
|
|
321
|
+
// failed to appear reads as a broken button.
|
|
343
322
|
if (messages.length === 0) {
|
|
344
323
|
return (
|
|
345
|
-
<div className="flex h-full items-center justify-center">
|
|
346
|
-
|
|
324
|
+
<div className="flex h-full items-center justify-center p-4">
|
|
325
|
+
{reply ? (
|
|
326
|
+
<ErrorState
|
|
327
|
+
variant="inline"
|
|
328
|
+
title="There is nothing here to answer"
|
|
329
|
+
error="This conversation has no messages left. Another mail client may have moved or deleted them."
|
|
330
|
+
/>
|
|
331
|
+
) : (
|
|
332
|
+
<EmptyState message="No messages in this thread" />
|
|
333
|
+
)}
|
|
347
334
|
</div>
|
|
348
335
|
);
|
|
349
336
|
}
|
|
@@ -355,7 +342,8 @@ export const ConversationView = ({
|
|
|
355
342
|
// its own px-5 inset (matches the AppShell ReadingPane geometry). On mobile
|
|
356
343
|
// each expanded card owns a per-message action bar. Its reply verbs are
|
|
357
344
|
// wired whatever the account's SMTP state: compose is where an account that
|
|
358
|
-
// cannot send says so.
|
|
345
|
+
// cannot send says so. A card's verb answers that card's message, which is
|
|
346
|
+
// the turn the address then names.
|
|
359
347
|
const renderMessages = (mobile: boolean) => (
|
|
360
348
|
<div data-testid="conversation-messages">
|
|
361
349
|
{orderedMessages.map((message, index) => (
|
|
@@ -376,9 +364,17 @@ export const ConversationView = ({
|
|
|
376
364
|
}
|
|
377
365
|
accountId={mailboxAccountId}
|
|
378
366
|
mobile={mobile}
|
|
379
|
-
onReply={
|
|
380
|
-
|
|
381
|
-
|
|
367
|
+
onReply={
|
|
368
|
+
mobile ? () => replyWith("reply", message.messageId) : undefined
|
|
369
|
+
}
|
|
370
|
+
onReplyAll={
|
|
371
|
+
mobile
|
|
372
|
+
? () => replyWith("reply-all", message.messageId)
|
|
373
|
+
: undefined
|
|
374
|
+
}
|
|
375
|
+
onForward={
|
|
376
|
+
mobile ? () => replyWith("forward", message.messageId) : undefined
|
|
377
|
+
}
|
|
382
378
|
/>
|
|
383
379
|
</div>
|
|
384
380
|
))}
|
|
@@ -389,14 +385,23 @@ export const ConversationView = ({
|
|
|
389
385
|
// is what the reader came back for, and it is not something to go looking
|
|
390
386
|
// for under a thread. It scrolls with the conversation rather than floating
|
|
391
387
|
// over it, so the pane has one scrollbar whether or not a reply is open.
|
|
392
|
-
|
|
388
|
+
//
|
|
389
|
+
// A segment naming no mode is a hand-typed address, and it says so where the
|
|
390
|
+
// composer would have been rather than opening nothing: the conversation
|
|
391
|
+
// behind it is still open and still readable.
|
|
392
|
+
const compose = reply && (
|
|
393
393
|
<div ref={composeRef}>
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
394
|
+
{reply.kind === "reply" ? (
|
|
395
|
+
<ConversationCompose surface={reply} />
|
|
396
|
+
) : (
|
|
397
|
+
<div className="border-b border-line bg-canvas p-4">
|
|
398
|
+
<ErrorState
|
|
399
|
+
variant="inline"
|
|
400
|
+
title="That is not a way to answer a message"
|
|
401
|
+
error={`"${reply.segment}" is not reply, reply-all or forward.`}
|
|
402
|
+
/>
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
400
405
|
</div>
|
|
401
406
|
);
|
|
402
407
|
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* \Drafts special-use folder (issue #505):
|
|
6
6
|
*
|
|
7
7
|
* 1. "Not yet sent (Remit)" — outbox rows with status === "draft" belonging
|
|
8
|
-
* to the account that owns the open \Drafts mailbox. Clicking a row
|
|
9
|
-
*
|
|
8
|
+
* to the account that owns the open \Drafts mailbox. Clicking a row names
|
|
9
|
+
* the draft and navigates to the folder's compose route.
|
|
10
10
|
*
|
|
11
11
|
* 2. "On the server" — IMAP \Drafts thread rows already loaded for the
|
|
12
12
|
* mailbox. Clicking a row opens the normal reading pane (read-only;
|
|
@@ -39,9 +39,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
39
39
|
import { useNavigate } from "@tanstack/react-router";
|
|
40
40
|
import { FileText, Inbox, Trash2 } from "lucide-react";
|
|
41
41
|
import { useMemo } from "react";
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
import { groupDraftSections } from "@/lib/drafts";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
useComposeDraftId,
|
|
46
|
+
useEditDraft,
|
|
47
|
+
useRetainOpenPanels,
|
|
48
|
+
} from "@/routing";
|
|
45
49
|
import { NavMenuButton } from "./NavMenuButton";
|
|
46
50
|
|
|
47
51
|
// ---------------------------------------------------------------------------
|
|
@@ -154,7 +158,8 @@ export function DraftsView({
|
|
|
154
158
|
title,
|
|
155
159
|
unreadCount,
|
|
156
160
|
}: DraftsViewProps) {
|
|
157
|
-
const
|
|
161
|
+
const openDraftId = useComposeDraftId();
|
|
162
|
+
const editDraft = useEditDraft();
|
|
158
163
|
const navigate = useNavigate();
|
|
159
164
|
const retainPanels = useRetainOpenPanels();
|
|
160
165
|
const queryClient = useQueryClient();
|
|
@@ -186,10 +191,6 @@ export function DraftsView({
|
|
|
186
191
|
deleteMutation.mutate({ path: { outboxMessageId } });
|
|
187
192
|
};
|
|
188
193
|
|
|
189
|
-
const handleRemitDraftOpen = (outboxMessageId: string) => {
|
|
190
|
-
openCompose({ mode: "new", outboxMessageId });
|
|
191
|
-
};
|
|
192
|
-
|
|
193
194
|
const handleImapDraftOpen = (messageId: string) => {
|
|
194
195
|
const threadId = imapThreads.find(
|
|
195
196
|
(thread) => thread.messageId === messageId,
|
|
@@ -246,11 +247,8 @@ export function DraftsView({
|
|
|
246
247
|
<RemitDraftRow
|
|
247
248
|
key={thread.id}
|
|
248
249
|
row={thread}
|
|
249
|
-
isSelected={
|
|
250
|
-
|
|
251
|
-
thread.id === composeState.outboxMessageId
|
|
252
|
-
}
|
|
253
|
-
onOpen={handleRemitDraftOpen}
|
|
250
|
+
isSelected={thread.id === openDraftId}
|
|
251
|
+
onOpen={editDraft}
|
|
254
252
|
onDelete={handleRemitDraftDelete}
|
|
255
253
|
isDeleting={deleteMutation.isPending}
|
|
256
254
|
/>
|
|
@@ -55,6 +55,10 @@ import { useMailContext } from "@/lib/mail-context";
|
|
|
55
55
|
import {
|
|
56
56
|
type OpenThreadPath,
|
|
57
57
|
type OpenThreadTarget,
|
|
58
|
+
type ReplyMode,
|
|
59
|
+
useIsComposing,
|
|
60
|
+
useIsReplying,
|
|
61
|
+
useOpenReply,
|
|
58
62
|
useRetainOpenPanels,
|
|
59
63
|
} from "@/routing";
|
|
60
64
|
|
|
@@ -81,6 +85,11 @@ interface FlaggedPaneContextValue {
|
|
|
81
85
|
* account — Flagged spans accounts, so there is no route mailbox to key by.
|
|
82
86
|
*/
|
|
83
87
|
actions: ThreadActions;
|
|
88
|
+
/**
|
|
89
|
+
* Answer the conversation the address has open. Absent when it names none,
|
|
90
|
+
* which is what the toolbar turns into its own explanation.
|
|
91
|
+
*/
|
|
92
|
+
onReply: ((mode: ReplyMode) => void) | undefined;
|
|
84
93
|
/** Keyboard, multi-select and next/previous, shared with the mailbox view. */
|
|
85
94
|
triage: TriageContext;
|
|
86
95
|
onDeleteMessages: (messageIds: string[]) => void;
|
|
@@ -210,15 +219,47 @@ function FlaggedPaneProvider({ thread, children }: FlaggedPaneProps) {
|
|
|
210
219
|
const triageTarget = focusedThread ?? selectedThread;
|
|
211
220
|
const triageActions = useThreadActions({ thread: triageTarget });
|
|
212
221
|
|
|
222
|
+
// The toolbar answers the conversation on screen; the keyboard answers the
|
|
223
|
+
// row the cursor is on, which may be one the address has not opened yet.
|
|
224
|
+
// Both are one navigation, because the mode and the message it answers are
|
|
225
|
+
// segments of the same address — there is no open-it-first step for the
|
|
226
|
+
// answering half to be dropped from.
|
|
227
|
+
const isComposing = useIsComposing();
|
|
228
|
+
const isReplying = useIsReplying();
|
|
229
|
+
const openReply = useOpenReply();
|
|
230
|
+
const replyToOpenThread = useMemo(() => {
|
|
231
|
+
if (!selectedThread || !selectedMessageId) return undefined;
|
|
232
|
+
return (mode: ReplyMode) =>
|
|
233
|
+
openReply({
|
|
234
|
+
threadId: selectedThread.threadId,
|
|
235
|
+
messageId: selectedMessageId,
|
|
236
|
+
mode,
|
|
237
|
+
});
|
|
238
|
+
}, [openReply, selectedThread, selectedMessageId]);
|
|
239
|
+
|
|
240
|
+
const replyToFocusedThread = useMemo(() => {
|
|
241
|
+
if (!triageTarget) return undefined;
|
|
242
|
+
return (mode: ReplyMode) =>
|
|
243
|
+
openReply({
|
|
244
|
+
threadId: triageTarget.threadId,
|
|
245
|
+
messageId: triageTarget.messageId,
|
|
246
|
+
mode,
|
|
247
|
+
});
|
|
248
|
+
}, [openReply, triageTarget]);
|
|
249
|
+
|
|
213
250
|
const { nextMessageId, previousMessageId } = useTriageLayer({
|
|
214
251
|
context: triage,
|
|
215
252
|
orderedIds: triage.orderedIds,
|
|
216
253
|
selectedMessageId,
|
|
254
|
+
// The list stays mounted under both writing surfaces, so the triage keys
|
|
255
|
+
// would otherwise fire at the message behind whatever is being typed — or
|
|
256
|
+
// answer a row the cursor moved to while a reply was open.
|
|
257
|
+
enabled: !isComposing && !isReplying,
|
|
217
258
|
onClose: handleCloseThread,
|
|
218
259
|
handlers: {
|
|
219
|
-
reply: () =>
|
|
220
|
-
replyAll: () =>
|
|
221
|
-
forward: () =>
|
|
260
|
+
reply: () => replyToFocusedThread?.("reply"),
|
|
261
|
+
replyAll: () => replyToFocusedThread?.("reply-all"),
|
|
262
|
+
forward: () => replyToFocusedThread?.("forward"),
|
|
222
263
|
// The list takes any verb aimed at its selection and opens the wizard
|
|
223
264
|
// on it, so a shortcut can never reach a bulk action the bar would have
|
|
224
265
|
// reviewed. What comes back here is a verb aimed at the bare cursor.
|
|
@@ -258,6 +299,7 @@ function FlaggedPaneProvider({ thread, children }: FlaggedPaneProps) {
|
|
|
258
299
|
nextThread: adjacentThread(nextMessageId),
|
|
259
300
|
previousThread: adjacentThread(previousMessageId),
|
|
260
301
|
actions,
|
|
302
|
+
onReply: replyToOpenThread,
|
|
261
303
|
triage,
|
|
262
304
|
onDeleteMessages: deleteMessages,
|
|
263
305
|
handleDeselectIfRemoved,
|
|
@@ -292,7 +334,7 @@ function FlaggedListSlot() {
|
|
|
292
334
|
* Mount in the `reading` slot of `AppShellSlotted`. Only rendered ≥ 1024px.
|
|
293
335
|
*/
|
|
294
336
|
function FlaggedReading() {
|
|
295
|
-
const { conversation, actions } = useFlaggedPane();
|
|
337
|
+
const { conversation, actions, onReply } = useFlaggedPane();
|
|
296
338
|
const { intelligenceOpen, onToggleIntelligence } = useMailContext();
|
|
297
339
|
// The rail's own width gate, not the shell tier: between 1024 and 1280 the
|
|
298
340
|
// reading pane is mounted but the rail is not, so "enabled" would promise an
|
|
@@ -308,13 +350,9 @@ function FlaggedReading() {
|
|
|
308
350
|
intelligenceOpen={canToggleIntelligence && intelligenceOpen}
|
|
309
351
|
canToggleIntelligence={canToggleIntelligence}
|
|
310
352
|
onToggleIntelligence={onToggleIntelligence}
|
|
311
|
-
onReply={
|
|
312
|
-
onReplyAll={
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
onForward={
|
|
316
|
-
hasThread ? () => actions.requestCompose("forward") : undefined
|
|
317
|
-
}
|
|
353
|
+
onReply={onReply ? () => onReply("reply") : undefined}
|
|
354
|
+
onReplyAll={onReply ? () => onReply("reply-all") : undefined}
|
|
355
|
+
onForward={onReply ? () => onReply("forward") : undefined}
|
|
318
356
|
onDelete={hasThread ? actions.deleteThread : undefined}
|
|
319
357
|
onToggleStar={hasThread ? actions.toggleStar : undefined}
|
|
320
358
|
isStarred={actions.isStarred}
|
|
@@ -336,8 +374,6 @@ function FlaggedReading() {
|
|
|
336
374
|
subject={conversation.subject}
|
|
337
375
|
selectedMessageId={conversation.messageId}
|
|
338
376
|
authenticity={conversation.authenticity}
|
|
339
|
-
composeRequest={actions.composeRequest}
|
|
340
|
-
onComposeClose={actions.clearComposeRequest}
|
|
341
377
|
/>
|
|
342
378
|
) : (
|
|
343
379
|
<ReadingPaneEmpty />
|