@remit/web-client 0.0.49 → 0.0.51

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.49",
3
+ "version": "0.0.51",
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": {
@@ -29,7 +29,7 @@
29
29
  "build:dist": "npm run generate:routes && node --import tsx harness/build.mjs",
30
30
  "preview": "vite preview",
31
31
  "test:typecheck": "npm run generate:routes && tsgo --noEmit",
32
- "test:run": "node --import tsx --import ./test-support/register.mjs --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=50 --test 'src/**/*.test.ts'",
32
+ "test:run": "node --import tsx --import ./test-support/register.mjs --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=70 --test 'src/**/*.test.ts'",
33
33
  "test": "npm run test:typecheck && npm run test:run"
34
34
  },
35
35
  "peerDependencies": {
@@ -97,8 +97,6 @@
97
97
  "@aws-sdk/client-dynamodb": "*",
98
98
  "@aws-sdk/client-sqs": "*",
99
99
  "@aws-sdk/lib-dynamodb": "*",
100
- "@types/nodemailer": "*",
101
- "nodemailer": "^9.0.3",
102
100
  "@playwright/test": "1.58.2",
103
101
  "@remit/drizzle-service": "*",
104
102
  "@remit/electrodb-entities": "*",
@@ -107,11 +105,15 @@
107
105
  "@tailwindcss/vite": "^4",
108
106
  "@tanstack/router-generator": "^1",
109
107
  "@tanstack/router-plugin": "^1",
108
+ "@types/jsdom": "^28.0.3",
109
+ "@types/nodemailer": "*",
110
110
  "@types/pngjs": "^6.0.5",
111
111
  "@vitejs/plugin-react": "^4",
112
112
  "electrodb": "*",
113
113
  "esbuild": "*",
114
114
  "expect-env": "*",
115
+ "jsdom": "^29.1.1",
116
+ "nodemailer": "^9.0.3",
115
117
  "pngjs": "^5",
116
118
  "react": "^19",
117
119
  "react-dom": "^19.2.4",
@@ -33,7 +33,14 @@ import { ConversationView } from "@/components/mail/ConversationView";
33
33
  import { DailyBrief } from "@/components/mail/DailyBrief";
34
34
  import { IntelligencePane } from "@/components/mail/IntelligencePane";
35
35
  import { MessageToolbar } from "@/components/mail/MessageToolbar";
36
+ import { useDeleteMessages } from "@/hooks/useDeleteMessages";
37
+ import { useToggleReadFor } from "@/hooks/useMarkAsRead";
36
38
  import { type ThreadActions, useThreadActions } from "@/hooks/useThreadActions";
39
+ import {
40
+ type TriageContext,
41
+ useTriageContext,
42
+ useTriageLayer,
43
+ } from "@/hooks/useTriageLayer";
37
44
  import {
38
45
  buildConversationTarget,
39
46
  type ConversationTarget,
@@ -57,6 +64,12 @@ interface BriefPaneContextValue {
57
64
  * account — the brief spans accounts, so there is no route mailbox to key by.
58
65
  */
59
66
  actions: ThreadActions;
67
+ /** Keyboard, multi-select and next/previous, shared with the mailbox view. */
68
+ triage: TriageContext;
69
+ onDeleteMessages: (messageIds: string[]) => void;
70
+ onMarkMessagesRead: (messageIds: string[]) => void;
71
+ nextMessageId: string | undefined;
72
+ previousMessageId: string | undefined;
60
73
  }
61
74
 
62
75
  const BriefPaneCtx = createContext<BriefPaneContextValue | null>(null);
@@ -176,6 +189,63 @@ function BriefPaneProvider({ selectedMessageId, children }: BriefPaneProps) {
176
189
  });
177
190
  }, [navigate]);
178
191
 
192
+ const triage = useTriageContext();
193
+
194
+ // A brief selection spans accounts and mailboxes, so the listings these
195
+ // patch are resolved from each message's own mailbox — the open thread's is
196
+ // only the fallback.
197
+ const briefThreads = useMemo(() => threadsData?.items ?? [], [threadsData]);
198
+ const { deleteMessages } = useDeleteMessages({
199
+ mailboxId: selectedThread?.mailboxId ?? "",
200
+ messages: briefThreads,
201
+ onAfterOptimisticRemove: handleDeselectIfRemoved,
202
+ });
203
+ const { toggleReadFor } = useToggleReadFor({
204
+ mailboxId: selectedThread?.mailboxId ?? "",
205
+ messages: briefThreads,
206
+ });
207
+ const handleMarkMessagesRead = useCallback(
208
+ (messageIds: string[]) => toggleReadFor(messageIds, true),
209
+ [toggleReadFor],
210
+ );
211
+
212
+ const focusedThreadId = triage.focusedMessageId;
213
+ const focusedThread = useMemo(
214
+ () => threadsData?.items.find((t) => t.messageId === focusedThreadId),
215
+ [threadsData, focusedThreadId],
216
+ );
217
+ const triageTarget = focusedThread ?? selectedThread;
218
+ const triageActions = useThreadActions({ thread: triageTarget });
219
+
220
+ const { nextMessageId, previousMessageId } = useTriageLayer({
221
+ context: triage,
222
+ orderedIds: triage.orderedIds,
223
+ selectedMessageId,
224
+ onClose: handleCloseThread,
225
+ handlers: {
226
+ reply: () => actions.requestCompose("reply"),
227
+ replyAll: () => actions.requestCompose("reply_all"),
228
+ forward: () => actions.requestCompose("forward"),
229
+ delete: () => {
230
+ if (triage.listCommandsRef.current?.requestDelete()) return;
231
+ triageActions.deleteThread();
232
+ },
233
+ toggleStar: triageActions.toggleStar,
234
+ toggleRead: () => {
235
+ const ids =
236
+ triage.selectedIds.length > 0
237
+ ? triage.selectedIds
238
+ : triageTarget
239
+ ? [triageTarget.messageId]
240
+ : [];
241
+ if (ids.length === 0) return;
242
+ toggleReadFor(ids, !(triageTarget?.isRead ?? false));
243
+ },
244
+ goFlagged: () => navigate({ to: "/mail/flagged" }),
245
+ goSettings: () => navigate({ to: "/settings" }),
246
+ },
247
+ });
248
+
179
249
  const ctx: BriefPaneContextValue = {
180
250
  selectedMessageId,
181
251
  selectedThread,
@@ -184,6 +254,11 @@ function BriefPaneProvider({ selectedMessageId, children }: BriefPaneProps) {
184
254
  onSelectSearchResult: handleSelectSearchResult,
185
255
  onCloseThread: handleCloseThread,
186
256
  actions,
257
+ triage,
258
+ onDeleteMessages: deleteMessages,
259
+ onMarkMessagesRead: handleMarkMessagesRead,
260
+ nextMessageId,
261
+ previousMessageId,
187
262
  };
188
263
 
189
264
  return <BriefPaneCtx.Provider value={ctx}>{children}</BriefPaneCtx.Provider>;
@@ -197,8 +272,14 @@ function BriefPaneProvider({ selectedMessageId, children }: BriefPaneProps) {
197
272
  * Daily brief list. Mount in the `list` slot of `AppShellSlotted`.
198
273
  */
199
274
  function BriefList() {
200
- const { selectedMessageId, onSelectMessage, onSelectSearchResult } =
201
- useBriefPane();
275
+ const {
276
+ selectedMessageId,
277
+ onSelectMessage,
278
+ onSelectSearchResult,
279
+ triage,
280
+ onDeleteMessages,
281
+ onMarkMessagesRead,
282
+ } = useBriefPane();
202
283
  const { accounts } = useMailContext();
203
284
 
204
285
  return (
@@ -207,6 +288,10 @@ function BriefList() {
207
288
  selectedMessageId={selectedMessageId}
208
289
  onSelectMessage={onSelectMessage}
209
290
  onSelectSearchResult={onSelectSearchResult}
291
+ commandsRef={triage.listCommandsRef}
292
+ onTriageContextChange={triage.onTriageContextChange}
293
+ onDeleteMessages={onDeleteMessages}
294
+ onMarkMessagesRead={onMarkMessagesRead}
210
295
  />
211
296
  );
212
297
  }
@@ -300,6 +385,8 @@ function BriefPhone() {
300
385
  onSelectMessage,
301
386
  onSelectSearchResult,
302
387
  onCloseThread,
388
+ nextMessageId,
389
+ previousMessageId,
303
390
  } = useBriefPane();
304
391
  const { accounts, intelligenceOpen, onToggleIntelligence } = useMailContext();
305
392
 
@@ -314,6 +401,15 @@ function BriefPhone() {
314
401
  authenticity={conversation.authenticity}
315
402
  onBack={onCloseThread}
316
403
  onOpenIntelligence={onToggleIntelligence}
404
+ onSwipeNext={
405
+ nextMessageId ? () => onSelectMessage(nextMessageId) : undefined
406
+ }
407
+ onSwipePrevious={
408
+ previousMessageId
409
+ ? () => onSelectMessage(previousMessageId)
410
+ : undefined
411
+ }
412
+ mobileIntelligenceOpen={intelligenceOpen}
317
413
  />
318
414
  <Drawer
319
415
  isOpen={intelligenceOpen}
@@ -34,10 +34,11 @@ import {
34
34
  import { useQueries, useQuery } from "@tanstack/react-query";
35
35
  import { useNavigate } from "@tanstack/react-router";
36
36
  import { AlertCircle, RefreshCw, Sparkles } from "lucide-react";
37
- import { useCallback, useMemo, useState } from "react";
37
+ import { type RefObject, useCallback, useMemo, useState } from "react";
38
38
  import { useIsDesktop } from "@/hooks/useMediaQuery";
39
39
  import { useSearchTokenContext } from "@/hooks/useSearchTokenContext";
40
40
  import { useSemanticSearch } from "@/hooks/useSemanticSearch";
41
+ import type { TriageContextUpdate } from "@/hooks/useTriageLayer";
41
42
  import { sortAccountsByCreatedAt } from "@/lib/account-order";
42
43
  import {
43
44
  groupBriefSections,
@@ -51,7 +52,12 @@ import { useMailContext } from "@/lib/mail-context";
51
52
  import { relatedSearchResults, rowToSearchResult } from "@/lib/search-result";
52
53
  import { parseSearchTokens } from "@/lib/search-tokens";
53
54
  import { MailListHeader } from "./MailListHeader";
55
+ import type { MessageListCommands } from "./MessageList";
54
56
  import { MessageRow } from "./MessageRow";
57
+ import {
58
+ ThreadListInteraction,
59
+ ThreadListSelectionBar,
60
+ } from "./ThreadListInteraction";
55
61
 
56
62
  /* The brief's attribute chips as predicates (mirrors the kit `briefFilterChips`
57
63
  ids) so the phone search takeover narrows results the same way the list does. */
@@ -134,6 +140,12 @@ interface DailyBriefProps {
134
140
  * so it opens even when its message isn't in the loaded brief list.
135
141
  */
136
142
  onSelectSearchResult?: (result: SearchResult) => void;
143
+ /** Where the list publishes the commands the keyboard layer drives. */
144
+ commandsRef?: RefObject<MessageListCommands | null>;
145
+ /** Cursor / selection / display order, reported up to the triage layer. */
146
+ onTriageContextChange?: (context: TriageContextUpdate) => void;
147
+ onDeleteMessages?: (messageIds: string[]) => void;
148
+ onMarkMessagesRead?: (messageIds: string[]) => void;
137
149
  }
138
150
 
139
151
  export function DailyBrief({
@@ -141,6 +153,10 @@ export function DailyBrief({
141
153
  selectedMessageId,
142
154
  onSelectMessage,
143
155
  onSelectSearchResult,
156
+ commandsRef,
157
+ onTriageContextChange,
158
+ onDeleteMessages,
159
+ onMarkMessagesRead,
144
160
  }: DailyBriefProps) {
145
161
  const { searchQuery, resultFolderIndex } = useMailContext();
146
162
  const tokenContext = useSearchTokenContext();
@@ -405,17 +421,33 @@ export function DailyBrief({
405
421
  <p className="text-xs text-fg-subtle">Nothing needs attention.</p>
406
422
  </div>
407
423
  ) : (
408
- <BriefSections
409
- sections={sections}
410
- Row={MessageRow}
411
- briefCategory={selectedCategory}
412
- onSelectBriefCategory={setSelectedCategory}
413
- sources={accountSources}
414
- sourcesNote={mutedCount > 0 ? `+${mutedCount} muted` : undefined}
415
- onSelectSource={setSelectedAccountId}
416
- selectedThreadId={selectedMessageId}
417
- onSelectThread={onSelectMessage}
418
- />
424
+ <ThreadListInteraction
425
+ selectedMessageId={selectedMessageId}
426
+ onOpen={(id) => onSelectMessage?.(id)}
427
+ onDeleteMessages={onDeleteMessages}
428
+ commandsRef={commandsRef}
429
+ onTriageContextChange={onTriageContextChange}
430
+ >
431
+ <div className="flex h-full min-h-0 flex-col">
432
+ {onDeleteMessages ? (
433
+ <ThreadListSelectionBar onMarkAsRead={onMarkMessagesRead} />
434
+ ) : null}
435
+ <div className="min-h-0 flex-1">
436
+ <BriefSections
437
+ sections={sections}
438
+ Row={MessageRow}
439
+ manageFocus={false}
440
+ briefCategory={selectedCategory}
441
+ onSelectBriefCategory={setSelectedCategory}
442
+ sources={accountSources}
443
+ sourcesNote={mutedCount > 0 ? `+${mutedCount} muted` : undefined}
444
+ onSelectSource={setSelectedAccountId}
445
+ selectedThreadId={selectedMessageId}
446
+ onSelectThread={onSelectMessage}
447
+ />
448
+ </div>
449
+ </div>
450
+ </ThreadListInteraction>
419
451
  );
420
452
 
421
453
  return (
@@ -15,16 +15,15 @@
15
15
  */
16
16
  import {
17
17
  flaggedFilterConfig,
18
- LIST_ROW_SELECTOR,
19
18
  MessageListPane,
20
19
  type ThreadRowData,
21
- useRovingFocus,
22
20
  } from "@remit/ui";
23
- import { useCallback, useMemo, useRef, useState } from "react";
21
+ import { type RefObject, useCallback, useMemo, useState } from "react";
24
22
  import { formatErrorMessage } from "@/components/ui/ErrorState";
25
23
  import { useIsDesktop } from "@/hooks/useMediaQuery";
26
24
  import { useSearchTokenContext } from "@/hooks/useSearchTokenContext";
27
25
  import { useStarredThreads } from "@/hooks/useStarredThreads";
26
+ import type { TriageContextUpdate } from "@/hooks/useTriageLayer";
28
27
  import {
29
28
  matchesBriefSearch,
30
29
  matchesSearchTokens,
@@ -36,7 +35,12 @@ import { rowToSearchResult } from "@/lib/search-result";
36
35
  import { parseSearchTokens } from "@/lib/search-tokens";
37
36
  import { dedupeByThread } from "@/lib/starred-rows";
38
37
  import { MailViewChrome } from "./MailViewChrome";
38
+ import type { MessageListCommands } from "./MessageList";
39
39
  import { MessageRow } from "./MessageRow";
40
+ import {
41
+ ThreadListInteraction,
42
+ ThreadListSelectionBar,
43
+ } from "./ThreadListInteraction";
40
44
 
41
45
  const FILTER_PREDICATES: Record<string, (t: ThreadRowData) => boolean> = {
42
46
  unread: (t) => !t.isRead,
@@ -46,19 +50,26 @@ const FILTER_PREDICATES: Record<string, (t: ThreadRowData) => boolean> = {
46
50
  interface FlaggedListProps {
47
51
  selectedMessageId?: string;
48
52
  onSelectMessage?: (id: string) => void;
53
+ /** Where the list publishes the commands the keyboard layer drives. */
54
+ commandsRef?: RefObject<MessageListCommands | null>;
55
+ /** Cursor / selection / display order, reported up to the triage layer. */
56
+ onTriageContextChange?: (context: TriageContextUpdate) => void;
57
+ onDeleteMessages?: (messageIds: string[]) => void;
58
+ onMarkMessagesRead?: (messageIds: string[]) => void;
49
59
  }
50
60
 
51
61
  export function FlaggedList({
52
62
  selectedMessageId,
53
63
  onSelectMessage,
64
+ commandsRef,
65
+ onTriageContextChange,
66
+ onDeleteMessages,
67
+ onMarkMessagesRead,
54
68
  }: FlaggedListProps) {
55
69
  const { searchQuery, resultFolderIndex } = useMailContext();
56
70
  const tokenContext = useSearchTokenContext();
57
71
  const isDesktop = useIsDesktop();
58
72
 
59
- const listRef = useRef<HTMLDivElement>(null);
60
- useRovingFocus({ containerRef: listRef, itemSelector: LIST_ROW_SELECTOR });
61
-
62
73
  const [selectedCategory, setSelectedCategory] = useState("all");
63
74
  const [activeFilters, setActiveFilters] = useState<ReadonlySet<string>>(
64
75
  new Set(),
@@ -135,28 +146,41 @@ export function FlaggedList({
135
146
  }, []);
136
147
 
137
148
  const listBody = (
138
- <div ref={listRef} className="flex-1 overflow-y-auto">
139
- <div className="divide-y divide-line">
140
- {rows.map((thread) => (
141
- <MessageRow
142
- key={thread.id}
143
- thread={thread}
144
- active={thread.id === selectedMessageId}
145
- onClick={() => onSelectMessage?.(thread.id)}
146
- />
147
- ))}
149
+ <ThreadListInteraction
150
+ selectedMessageId={selectedMessageId}
151
+ onOpen={(id) => onSelectMessage?.(id)}
152
+ onDeleteMessages={onDeleteMessages}
153
+ commandsRef={commandsRef}
154
+ onTriageContextChange={onTriageContextChange}
155
+ >
156
+ <div className="flex h-full min-h-0 flex-col">
157
+ {onDeleteMessages ? (
158
+ <ThreadListSelectionBar onMarkAsRead={onMarkMessagesRead} />
159
+ ) : null}
160
+ <div className="flex-1 overflow-y-auto">
161
+ <div className="divide-y divide-line">
162
+ {rows.map((thread) => (
163
+ <MessageRow
164
+ key={thread.id}
165
+ thread={thread}
166
+ active={thread.id === selectedMessageId}
167
+ onClick={() => onSelectMessage?.(thread.id)}
168
+ />
169
+ ))}
170
+ </div>
171
+ {hasNextPage ? (
172
+ <button
173
+ type="button"
174
+ className="w-full py-3 text-sm text-muted hover:text-fg disabled:opacity-50"
175
+ onClick={() => fetchNextPage()}
176
+ disabled={isFetchingNextPage}
177
+ >
178
+ {isFetchingNextPage ? "Loading…" : "Load more"}
179
+ </button>
180
+ ) : null}
181
+ </div>
148
182
  </div>
149
- {hasNextPage ? (
150
- <button
151
- type="button"
152
- className="w-full py-3 text-sm text-muted hover:text-fg disabled:opacity-50"
153
- onClick={() => fetchNextPage()}
154
- disabled={isFetchingNextPage}
155
- >
156
- {isFetchingNextPage ? "Loading…" : "Load more"}
157
- </button>
158
- ) : null}
159
- </div>
183
+ </ThreadListInteraction>
160
184
  );
161
185
 
162
186
  return (
@@ -35,8 +35,15 @@ import { ConversationView } from "@/components/mail/ConversationView";
35
35
  import { FlaggedList } from "@/components/mail/FlaggedList";
36
36
  import { IntelligencePane } from "@/components/mail/IntelligencePane";
37
37
  import { MessageToolbar } from "@/components/mail/MessageToolbar";
38
+ import { useDeleteMessages } from "@/hooks/useDeleteMessages";
39
+ import { useToggleReadFor } from "@/hooks/useMarkAsRead";
38
40
  import { useStarredThreads } from "@/hooks/useStarredThreads";
39
41
  import { type ThreadActions, useThreadActions } from "@/hooks/useThreadActions";
42
+ import {
43
+ type TriageContext,
44
+ useTriageContext,
45
+ useTriageLayer,
46
+ } from "@/hooks/useTriageLayer";
40
47
  import { useMailContext } from "@/lib/mail-context";
41
48
 
42
49
  /* ------------------------------------------------------------------ */
@@ -53,6 +60,12 @@ interface FlaggedPaneContextValue {
53
60
  * account — Flagged spans accounts, so there is no route mailbox to key by.
54
61
  */
55
62
  actions: ThreadActions;
63
+ /** Keyboard, multi-select and next/previous, shared with the mailbox view. */
64
+ triage: TriageContext;
65
+ onDeleteMessages: (messageIds: string[]) => void;
66
+ onMarkMessagesRead: (messageIds: string[]) => void;
67
+ nextMessageId: string | undefined;
68
+ previousMessageId: string | undefined;
56
69
  }
57
70
 
58
71
  const FlaggedPaneCtx = createContext<FlaggedPaneContextValue | null>(null);
@@ -116,12 +129,73 @@ function FlaggedPaneProvider({
116
129
  onAfterOptimisticRemove: handleDeselectIfRemoved,
117
130
  });
118
131
 
132
+ const triage = useTriageContext();
133
+
134
+ // A Flagged selection spans accounts and mailboxes, so the listings these
135
+ // patch are resolved from each message's own mailbox — the open thread's is
136
+ // only the fallback.
137
+ const { deleteMessages } = useDeleteMessages({
138
+ mailboxId: selectedThread?.mailboxId ?? "",
139
+ messages: threads,
140
+ onAfterOptimisticRemove: handleDeselectIfRemoved,
141
+ });
142
+ const { toggleReadFor } = useToggleReadFor({
143
+ mailboxId: selectedThread?.mailboxId ?? "",
144
+ messages: threads,
145
+ });
146
+ const handleMarkMessagesRead = useCallback(
147
+ (messageIds: string[]) => toggleReadFor(messageIds, true),
148
+ [toggleReadFor],
149
+ );
150
+
151
+ const focusedThreadId = triage.focusedMessageId;
152
+ const focusedThread = useMemo(
153
+ () => threads.find((t) => t.messageId === focusedThreadId),
154
+ [threads, focusedThreadId],
155
+ );
156
+ const triageTarget = focusedThread ?? selectedThread;
157
+ const triageActions = useThreadActions({ thread: triageTarget });
158
+
159
+ const { nextMessageId, previousMessageId } = useTriageLayer({
160
+ context: triage,
161
+ orderedIds: triage.orderedIds,
162
+ selectedMessageId,
163
+ onClose: handleCloseThread,
164
+ handlers: {
165
+ reply: () => actions.requestCompose("reply"),
166
+ replyAll: () => actions.requestCompose("reply_all"),
167
+ forward: () => actions.requestCompose("forward"),
168
+ delete: () => {
169
+ if (triage.listCommandsRef.current?.requestDelete()) return;
170
+ triageActions.deleteThread();
171
+ },
172
+ toggleStar: triageActions.toggleStar,
173
+ toggleRead: () => {
174
+ const ids =
175
+ triage.selectedIds.length > 0
176
+ ? triage.selectedIds
177
+ : triageTarget
178
+ ? [triageTarget.messageId]
179
+ : [];
180
+ if (ids.length === 0) return;
181
+ toggleReadFor(ids, !(triageTarget?.isRead ?? false));
182
+ },
183
+ goBrief: () => navigate({ to: "/mail" }),
184
+ goSettings: () => navigate({ to: "/settings" }),
185
+ },
186
+ });
187
+
119
188
  const ctx: FlaggedPaneContextValue = {
120
189
  selectedMessageId,
121
190
  selectedThread,
122
191
  onSelectMessage: handleSelectMessage,
123
192
  onCloseThread: handleCloseThread,
124
193
  actions,
194
+ triage,
195
+ onDeleteMessages: deleteMessages,
196
+ onMarkMessagesRead: handleMarkMessagesRead,
197
+ nextMessageId,
198
+ previousMessageId,
125
199
  };
126
200
 
127
201
  return (
@@ -135,11 +209,21 @@ function FlaggedPaneProvider({
135
209
 
136
210
  /** Flat starred list. Mount in the `list` slot of `AppShellSlotted`. */
137
211
  function FlaggedListSlot() {
138
- const { selectedMessageId, onSelectMessage } = useFlaggedPane();
212
+ const {
213
+ selectedMessageId,
214
+ onSelectMessage,
215
+ triage,
216
+ onDeleteMessages,
217
+ onMarkMessagesRead,
218
+ } = useFlaggedPane();
139
219
  return (
140
220
  <FlaggedList
141
221
  selectedMessageId={selectedMessageId}
142
222
  onSelectMessage={onSelectMessage}
223
+ commandsRef={triage.listCommandsRef}
224
+ onTriageContextChange={triage.onTriageContextChange}
225
+ onDeleteMessages={onDeleteMessages}
226
+ onMarkMessagesRead={onMarkMessagesRead}
143
227
  />
144
228
  );
145
229
  }
@@ -223,7 +307,13 @@ function FlaggedIntelligence() {
223
307
 
224
308
  /** Phone view: ConversationView when a thread is open, else the flat list. */
225
309
  function FlaggedPhone() {
226
- const { selectedThread, onCloseThread } = useFlaggedPane();
310
+ const {
311
+ selectedThread,
312
+ onCloseThread,
313
+ onSelectMessage,
314
+ nextMessageId,
315
+ previousMessageId,
316
+ } = useFlaggedPane();
227
317
  const { intelligenceOpen, onToggleIntelligence } = useMailContext();
228
318
 
229
319
  if (selectedThread) {
@@ -236,6 +326,15 @@ function FlaggedPhone() {
236
326
  authenticity={selectedThread.authenticity}
237
327
  onBack={onCloseThread}
238
328
  onOpenIntelligence={onToggleIntelligence}
329
+ onSwipeNext={
330
+ nextMessageId ? () => onSelectMessage(nextMessageId) : undefined
331
+ }
332
+ onSwipePrevious={
333
+ previousMessageId
334
+ ? () => onSelectMessage(previousMessageId)
335
+ : undefined
336
+ }
337
+ mobileIntelligenceOpen={intelligenceOpen}
239
338
  />
240
339
  <Drawer
241
340
  isOpen={intelligenceOpen}