@remit/web-client 0.0.179 → 0.0.181

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.179",
3
+ "version": "0.0.181",
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": {
@@ -373,6 +373,24 @@ export const ComposeForm = ({
373
373
  * the subject the reader edited.
374
374
  */
375
375
  const resumedDraftRef = useRef(outboxMessageId !== undefined);
376
+ /**
377
+ * The identity this form has already taken from the surface that mounted it.
378
+ * A reply learns which account the message reached only once the mailbox it
379
+ * was delivered to resolves, which lands after the first render — so From
380
+ * follows the prop instead of only its initial value. Applied once per
381
+ * identity, so a reader who then picks another one keeps it.
382
+ */
383
+ const appliedAccountIdRef = useRef(account?.accountId);
384
+
385
+ useEffect(() => {
386
+ const accountId = account?.accountId;
387
+ if (!accountId || appliedAccountIdRef.current === accountId) return;
388
+ appliedAccountIdRef.current = accountId;
389
+ // A saved draft carries the account it was written from; that is the one
390
+ // it reopens on.
391
+ if (resumedDraftRef.current) return;
392
+ setSelectedAccountId(accountId);
393
+ }, [account?.accountId]);
376
394
 
377
395
  // The document this form is on changed under it, so it starts again: another
378
396
  // draft, or no draft at all. Without this the previous one's fields stay on
@@ -3,6 +3,7 @@ import {
3
3
  messageOperationsDescribeMessageOptions,
4
4
  } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
5
5
  import { useQuery } from "@tanstack/react-query";
6
+ import { useMailboxAccount } from "@/hooks/useMailboxAccount";
6
7
  import {
7
8
  type ReplyAddress,
8
9
  useAdoptReplyDraft,
@@ -38,6 +39,17 @@ export const ConversationCompose = ({ surface }: { surface: ReplyAddress }) => {
38
39
  staleTime: Infinity,
39
40
  });
40
41
 
42
+ // The answer leaves from the identity the message reached, so the account is
43
+ // the one owning the mailbox it was delivered to. Taking the head of the
44
+ // configured list answered every account's mail from the first identity, and
45
+ // left the reader's own address in the Cc of a Reply All (#819).
46
+ const { accountId: deliveredToAccountId } = useMailboxAccount(
47
+ sourceMessage?.message.mailboxId,
48
+ );
49
+ const account = config?.accounts?.find(
50
+ (candidate) => candidate.accountId === deliveredToAccountId,
51
+ );
52
+
41
53
  return (
42
54
  <div className="border-b border-line bg-canvas">
43
55
  {/* No key on the draft: the first autosave writes the id it created into
@@ -46,7 +58,7 @@ export const ConversationCompose = ({ surface }: { surface: ReplyAddress }) => {
46
58
  <ComposeForm
47
59
  layout="flow"
48
60
  mode={surface.mode}
49
- account={config?.accounts?.[0]}
61
+ account={account}
50
62
  sourceMessage={sourceMessage}
51
63
  outboxMessageId={surface.outboxMessageId}
52
64
  onDraftCreated={adoptCreatedDraft}
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { unifiedThreadOperationsListAllThreadsOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
16
16
  import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
17
- import { ReadingPaneEmpty, useAppShellLayout } from "@remit/ui";
17
+ import { ReadingPaneEmpty } from "@remit/ui";
18
18
  import { useQuery } from "@tanstack/react-query";
19
19
  import { useNavigate } from "@tanstack/react-router";
20
20
  import {
@@ -24,14 +24,15 @@ import {
24
24
  useContext,
25
25
  useMemo,
26
26
  } from "react";
27
- import { Drawer } from "@/components/layout/Drawer";
28
27
  import { ConversationView } from "@/components/mail/ConversationView";
29
28
  import { DailyBrief } from "@/components/mail/DailyBrief";
29
+ import { IntelligenceDrawer } from "@/components/mail/IntelligenceDrawer";
30
30
  import { IntelligencePane } from "@/components/mail/IntelligencePane";
31
31
  import { MessageToolbar } from "@/components/mail/MessageToolbar";
32
32
  import type { OpenMessageOptions } from "@/components/mail/ThreadListInteraction";
33
33
  import { useDeleteMessages } from "@/hooks/useDeleteMessages";
34
34
  import { useIntelligenceDrawer } from "@/hooks/useIntelligenceDrawer";
35
+ import { useIntelligenceSurface } from "@/hooks/useIntelligenceSurface";
35
36
  import { useToggleReadFor } from "@/hooks/useMarkAsRead";
36
37
  import { type ThreadActions, useThreadActions } from "@/hooks/useThreadActions";
37
38
  import { useThreadRow } from "@/hooks/useThreadRow";
@@ -328,52 +329,64 @@ function BriefList() {
328
329
  * Mount in the `reading` slot of `AppShellSlotted`. Only rendered ≥ 1024px.
329
330
  */
330
331
  function BriefReading() {
331
- const { conversation, actions, onReply } = useBriefPane();
332
- const { intelligenceOpen, onToggleIntelligence } = useMailContext();
333
- // The rail's own width gate, not the shell tier: between 1024 and 1280 the
334
- // reading pane is mounted but the rail is not, so "enabled" would promise an
335
- // open that cannot happen.
336
- const railFits = useAppShellLayout()?.showIntelligencePane ?? false;
332
+ const {
333
+ conversation,
334
+ selectedThread,
335
+ actions,
336
+ onReply,
337
+ handleDeselectIfRemoved,
338
+ } = useBriefPane();
337
339
  const hasThread = Boolean(conversation);
338
- const canToggleIntelligence = railFits && hasThread;
340
+ const intelligence = useIntelligenceSurface(conversation?.threadId);
339
341
 
340
342
  return (
341
- <section className="flex h-full w-full min-w-0 flex-col bg-canvas">
342
- <MessageToolbar
343
- hasThread={hasThread}
344
- intelligenceOpen={canToggleIntelligence && intelligenceOpen}
345
- canToggleIntelligence={canToggleIntelligence}
346
- onToggleIntelligence={onToggleIntelligence}
347
- onReply={onReply ? () => onReply("reply") : undefined}
348
- onReplyAll={onReply ? () => onReply("reply-all") : undefined}
349
- onForward={onReply ? () => onReply("forward") : undefined}
350
- onDelete={hasThread ? actions.deleteThread : undefined}
351
- onToggleStar={hasThread ? actions.toggleStar : undefined}
352
- isStarred={actions.isStarred}
353
- moveContext={
354
- hasThread && actions.accountId && actions.mailboxId
355
- ? {
356
- accountId: actions.accountId,
357
- currentMailboxId: actions.mailboxId,
358
- onMove: actions.moveThread,
359
- }
360
- : undefined
361
- }
343
+ <>
344
+ <section className="flex h-full w-full min-w-0 flex-col bg-canvas">
345
+ <MessageToolbar
346
+ hasThread={hasThread}
347
+ intelligenceOpen={intelligence.isShowing}
348
+ canToggleIntelligence={intelligence.canToggle}
349
+ onToggleIntelligence={intelligence.toggle}
350
+ onReply={onReply ? () => onReply("reply") : undefined}
351
+ onReplyAll={onReply ? () => onReply("reply-all") : undefined}
352
+ onForward={onReply ? () => onReply("forward") : undefined}
353
+ onDelete={hasThread ? actions.deleteThread : undefined}
354
+ onToggleStar={hasThread ? actions.toggleStar : undefined}
355
+ isStarred={actions.isStarred}
356
+ moveContext={
357
+ hasThread && actions.accountId && actions.mailboxId
358
+ ? {
359
+ accountId: actions.accountId,
360
+ currentMailboxId: actions.mailboxId,
361
+ onMove: actions.moveThread,
362
+ }
363
+ : undefined
364
+ }
365
+ />
366
+ <div className="min-h-0 flex-1 overflow-hidden">
367
+ {conversation ? (
368
+ <ConversationView
369
+ threadId={conversation.threadId}
370
+ mailboxId={conversation.mailboxId}
371
+ subject={conversation.subject}
372
+ selectedMessageId={conversation.messageId}
373
+ authenticity={conversation.authenticity}
374
+ onOpenIntelligence={intelligence.open}
375
+ />
376
+ ) : (
377
+ <ReadingPaneEmpty />
378
+ )}
379
+ </div>
380
+ </section>
381
+ <IntelligenceDrawer
382
+ isOpen={intelligence.drawerOpen}
383
+ onClose={intelligence.closeDrawer}
384
+ thread={selectedThread}
385
+ mailboxId={selectedThread?.mailboxId}
386
+ accountId={selectedThread?.accountId}
387
+ onAfterOptimisticRemove={handleDeselectIfRemoved}
362
388
  />
363
- <div className="min-h-0 flex-1 overflow-hidden">
364
- {conversation ? (
365
- <ConversationView
366
- threadId={conversation.threadId}
367
- mailboxId={conversation.mailboxId}
368
- subject={conversation.subject}
369
- selectedMessageId={conversation.messageId}
370
- authenticity={conversation.authenticity}
371
- />
372
- ) : (
373
- <ReadingPaneEmpty />
374
- )}
375
- </div>
376
- </section>
389
+ </>
377
390
  );
378
391
  }
379
392
 
@@ -428,21 +441,14 @@ function BriefPhone() {
428
441
  }
429
442
  mobileIntelligenceOpen={drawer.isOpen}
430
443
  />
431
- <Drawer
444
+ <IntelligenceDrawer
432
445
  isOpen={drawer.isOpen}
433
446
  onClose={drawer.close}
434
- ariaLabel="Message details"
435
- side="right"
436
- >
437
- <IntelligencePane
438
- onClose={drawer.close}
439
- thread={selectedThread}
440
- mailboxId={selectedThread?.mailboxId}
441
- accountId={selectedThread?.accountId}
442
- hideCloseButton
443
- onAfterOptimisticRemove={handleDeselectIfRemoved}
444
- />
445
- </Drawer>
447
+ thread={selectedThread}
448
+ mailboxId={selectedThread?.mailboxId}
449
+ accountId={selectedThread?.accountId}
450
+ onAfterOptimisticRemove={handleDeselectIfRemoved}
451
+ />
446
452
  </>
447
453
  );
448
454
  }
@@ -1,7 +1,4 @@
1
- import {
2
- configOperationsGetConfigOptions,
3
- threadDetailOperationsListThreadMessagesOptions,
4
- } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
1
+ import { threadDetailOperationsListThreadMessagesOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
5
2
  import type { RemitImapMessageAuthenticity } from "@remit/api-http-client/types.gen.ts";
6
3
  import { MobileReadingPane } from "@remit/ui";
7
4
  import { useQuery } from "@tanstack/react-query";
@@ -196,19 +193,17 @@ export const ConversationView = ({
196
193
  }
197
194
  }, [orderedMessages, focusedIndex, toggleExpanded]);
198
195
 
199
- const { data: config } = useQuery({
200
- ...configOperationsGetConfigOptions(),
201
- staleTime: Infinity,
202
- });
203
- const activeAccount = config?.accounts?.[0];
204
-
205
- // Mark messages as read immediately when expanded.
196
+ // Mark messages as read immediately when expanded. The unread badge that has
197
+ // to be refreshed belongs to the account this mailbox is in, so the
198
+ // invalidation is scoped to that one rather than to the first configured
199
+ // account, whose list was being refetched while the right one went stale
200
+ // (#819).
206
201
  useMarkAsRead({
207
202
  messages,
208
203
  expandedIds,
209
204
  threadId,
210
205
  mailboxId,
211
- accountId: activeAccount?.accountId,
206
+ accountId: mailboxAccountId,
212
207
  });
213
208
 
214
209
  // Star toggle functionality
@@ -25,7 +25,7 @@
25
25
  * On phone, use `<FlaggedPane.Phone />` instead.
26
26
  */
27
27
  import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
28
- import { ReadingPaneEmpty, useAppShellLayout } from "@remit/ui";
28
+ import { ReadingPaneEmpty } from "@remit/ui";
29
29
  import { useNavigate } from "@tanstack/react-router";
30
30
  import {
31
31
  createContext,
@@ -34,14 +34,15 @@ import {
34
34
  useContext,
35
35
  useMemo,
36
36
  } from "react";
37
- import { Drawer } from "@/components/layout/Drawer";
38
37
  import { ConversationView } from "@/components/mail/ConversationView";
39
38
  import { FlaggedList } from "@/components/mail/FlaggedList";
39
+ import { IntelligenceDrawer } from "@/components/mail/IntelligenceDrawer";
40
40
  import { IntelligencePane } from "@/components/mail/IntelligencePane";
41
41
  import { MessageToolbar } from "@/components/mail/MessageToolbar";
42
42
  import type { OpenMessageOptions } from "@/components/mail/ThreadListInteraction";
43
43
  import { useDeleteMessages } from "@/hooks/useDeleteMessages";
44
44
  import { useIntelligenceDrawer } from "@/hooks/useIntelligenceDrawer";
45
+ import { useIntelligenceSurface } from "@/hooks/useIntelligenceSurface";
45
46
  import { useToggleReadFor } from "@/hooks/useMarkAsRead";
46
47
  import { useStarredThreads } from "@/hooks/useStarredThreads";
47
48
  import { type ThreadActions, useThreadActions } from "@/hooks/useThreadActions";
@@ -331,52 +332,64 @@ function FlaggedListSlot() {
331
332
  * Mount in the `reading` slot of `AppShellSlotted`. Only rendered ≥ 1024px.
332
333
  */
333
334
  function FlaggedReading() {
334
- const { conversation, actions, onReply } = useFlaggedPane();
335
- const { intelligenceOpen, onToggleIntelligence } = useMailContext();
336
- // The rail's own width gate, not the shell tier: between 1024 and 1280 the
337
- // reading pane is mounted but the rail is not, so "enabled" would promise an
338
- // open that cannot happen.
339
- const railFits = useAppShellLayout()?.showIntelligencePane ?? false;
335
+ const {
336
+ conversation,
337
+ selectedThread,
338
+ actions,
339
+ onReply,
340
+ handleDeselectIfRemoved,
341
+ } = useFlaggedPane();
340
342
  const hasThread = Boolean(conversation);
341
- const canToggleIntelligence = railFits && hasThread;
343
+ const intelligence = useIntelligenceSurface(conversation?.threadId);
342
344
 
343
345
  return (
344
- <section className="flex h-full w-full min-w-0 flex-col bg-canvas">
345
- <MessageToolbar
346
- hasThread={hasThread}
347
- intelligenceOpen={canToggleIntelligence && intelligenceOpen}
348
- canToggleIntelligence={canToggleIntelligence}
349
- onToggleIntelligence={onToggleIntelligence}
350
- onReply={onReply ? () => onReply("reply") : undefined}
351
- onReplyAll={onReply ? () => onReply("reply-all") : undefined}
352
- onForward={onReply ? () => onReply("forward") : undefined}
353
- onDelete={hasThread ? actions.deleteThread : undefined}
354
- onToggleStar={hasThread ? actions.toggleStar : undefined}
355
- isStarred={actions.isStarred}
356
- moveContext={
357
- hasThread && actions.accountId && actions.mailboxId
358
- ? {
359
- accountId: actions.accountId,
360
- currentMailboxId: actions.mailboxId,
361
- onMove: actions.moveThread,
362
- }
363
- : undefined
364
- }
346
+ <>
347
+ <section className="flex h-full w-full min-w-0 flex-col bg-canvas">
348
+ <MessageToolbar
349
+ hasThread={hasThread}
350
+ intelligenceOpen={intelligence.isShowing}
351
+ canToggleIntelligence={intelligence.canToggle}
352
+ onToggleIntelligence={intelligence.toggle}
353
+ onReply={onReply ? () => onReply("reply") : undefined}
354
+ onReplyAll={onReply ? () => onReply("reply-all") : undefined}
355
+ onForward={onReply ? () => onReply("forward") : undefined}
356
+ onDelete={hasThread ? actions.deleteThread : undefined}
357
+ onToggleStar={hasThread ? actions.toggleStar : undefined}
358
+ isStarred={actions.isStarred}
359
+ moveContext={
360
+ hasThread && actions.accountId && actions.mailboxId
361
+ ? {
362
+ accountId: actions.accountId,
363
+ currentMailboxId: actions.mailboxId,
364
+ onMove: actions.moveThread,
365
+ }
366
+ : undefined
367
+ }
368
+ />
369
+ <div className="min-h-0 flex-1 overflow-hidden">
370
+ {conversation ? (
371
+ <ConversationView
372
+ threadId={conversation.threadId}
373
+ mailboxId={conversation.mailboxId}
374
+ subject={conversation.subject}
375
+ selectedMessageId={conversation.messageId}
376
+ authenticity={conversation.authenticity}
377
+ onOpenIntelligence={intelligence.open}
378
+ />
379
+ ) : (
380
+ <ReadingPaneEmpty />
381
+ )}
382
+ </div>
383
+ </section>
384
+ <IntelligenceDrawer
385
+ isOpen={intelligence.drawerOpen}
386
+ onClose={intelligence.closeDrawer}
387
+ thread={selectedThread}
388
+ mailboxId={selectedThread?.mailboxId}
389
+ accountId={selectedThread?.accountId}
390
+ onAfterOptimisticRemove={handleDeselectIfRemoved}
365
391
  />
366
- <div className="min-h-0 flex-1 overflow-hidden">
367
- {conversation ? (
368
- <ConversationView
369
- threadId={conversation.threadId}
370
- mailboxId={conversation.mailboxId}
371
- subject={conversation.subject}
372
- selectedMessageId={conversation.messageId}
373
- authenticity={conversation.authenticity}
374
- />
375
- ) : (
376
- <ReadingPaneEmpty />
377
- )}
378
- </div>
379
- </section>
392
+ </>
380
393
  );
381
394
  }
382
395
 
@@ -429,21 +442,14 @@ function FlaggedPhone() {
429
442
  }
430
443
  mobileIntelligenceOpen={drawer.isOpen}
431
444
  />
432
- <Drawer
445
+ <IntelligenceDrawer
433
446
  isOpen={drawer.isOpen}
434
447
  onClose={drawer.close}
435
- ariaLabel="Message details"
436
- side="right"
437
- >
438
- <IntelligencePane
439
- onClose={drawer.close}
440
- thread={selectedThread}
441
- mailboxId={selectedThread?.mailboxId}
442
- accountId={selectedThread?.accountId}
443
- hideCloseButton
444
- onAfterOptimisticRemove={handleDeselectIfRemoved}
445
- />
446
- </Drawer>
448
+ thread={selectedThread}
449
+ mailboxId={selectedThread?.mailboxId}
450
+ accountId={selectedThread?.accountId}
451
+ onAfterOptimisticRemove={handleDeselectIfRemoved}
452
+ />
447
453
  </>
448
454
  );
449
455
  }
@@ -0,0 +1,44 @@
1
+ import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
2
+ import { Drawer } from "@/components/layout/Drawer";
3
+ import { IntelligencePane } from "@/components/mail/IntelligencePane";
4
+
5
+ /**
6
+ * The intelligence pane as a modal drawer — the surface every list pane uses
7
+ * wherever the rail has no room, which is the phone and the two-pane desktop
8
+ * band between 1024 and 1280px. Its open state is the caller's
9
+ * (`useIntelligenceSurface`); what is inside it is the same panel the rail
10
+ * mounts, minus its own close, because the drawer's header already carries one.
11
+ */
12
+ export function IntelligenceDrawer({
13
+ isOpen,
14
+ onClose,
15
+ thread,
16
+ mailboxId,
17
+ accountId,
18
+ onAfterOptimisticRemove,
19
+ }: {
20
+ isOpen: boolean;
21
+ onClose: () => void;
22
+ thread?: RemitImapThreadMessageResponse;
23
+ mailboxId?: string;
24
+ accountId?: string;
25
+ onAfterOptimisticRemove?: (messageIds: string[]) => void;
26
+ }) {
27
+ return (
28
+ <Drawer
29
+ isOpen={isOpen}
30
+ onClose={onClose}
31
+ ariaLabel="Message details"
32
+ side="right"
33
+ >
34
+ <IntelligencePane
35
+ onClose={onClose}
36
+ thread={thread}
37
+ mailboxId={mailboxId}
38
+ accountId={accountId}
39
+ hideCloseButton
40
+ onAfterOptimisticRemove={onAfterOptimisticRemove}
41
+ />
42
+ </Drawer>
43
+ );
44
+ }
@@ -31,7 +31,6 @@ import {
31
31
  RefreshButton,
32
32
  type RescueCandidate,
33
33
  type SearchResult,
34
- useAppShellLayout,
35
34
  } from "@remit/ui";
36
35
  import { useInfiniteQuery } from "@tanstack/react-query";
37
36
  import { useNavigate } from "@tanstack/react-router";
@@ -46,9 +45,9 @@ import {
46
45
  useRef,
47
46
  useState,
48
47
  } from "react";
49
- import { Drawer } from "@/components/layout/Drawer";
50
48
  import { ConversationView } from "@/components/mail/ConversationView";
51
49
  import { DraftsView } from "@/components/mail/DraftsView";
50
+ import { IntelligenceDrawer } from "@/components/mail/IntelligenceDrawer";
52
51
  import { IntelligencePane } from "@/components/mail/IntelligencePane";
53
52
  import {
54
53
  MessageList,
@@ -73,6 +72,7 @@ import {
73
72
  import type { EscalationSearchQuery } from "@/hooks/useEscalatedActions";
74
73
  import { useIntelligenceData } from "@/hooks/useIntelligenceData";
75
74
  import { useIntelligenceDrawer } from "@/hooks/useIntelligenceDrawer";
75
+ import { useIntelligenceSurface } from "@/hooks/useIntelligenceSurface";
76
76
  import { useLayoutTier } from "@/hooks/useLayoutTier";
77
77
  import { useMailboxAccount } from "@/hooks/useMailboxAccount";
78
78
  import { useToggleReadFor } from "@/hooks/useMarkAsRead";
@@ -1059,19 +1059,14 @@ function MailboxReading() {
1059
1059
  mailboxAccountLoading,
1060
1060
  selectedThread,
1061
1061
  conversation,
1062
- intelligenceOpen,
1063
- onToggleIntelligence,
1064
1062
  onReply,
1065
1063
  onToolbarDelete,
1066
1064
  onToolbarStar,
1067
1065
  onToolbarMove,
1068
1066
  handleDeselectIfRemoved,
1069
1067
  } = useMailboxPane();
1070
- // Which surface intelligence has here: the rail between 1280 and up, the
1071
- // mobile drawer below that, where the reading pane is mounted and the rail
1072
- // has no room.
1073
- const railFits = useAppShellLayout()?.showIntelligencePane ?? false;
1074
1068
  const hasThread = Boolean(conversation);
1069
+ const intelligence = useIntelligenceSurface(conversation?.threadId);
1075
1070
 
1076
1071
  // The rail opens itself on a DKIM mismatch. It lives here, behind the rail's
1077
1072
  // own width gate, because raising it writes `#intelligence` into the address
@@ -1079,6 +1074,7 @@ function MailboxReading() {
1079
1074
  // opens nothing (`docs/architecture/url-state.md`, R6). Below this width the
1080
1075
  // banner is the announcement and its "Why?" is the way in.
1081
1076
  const autoOpenedForRef = useRef<string | null>(null);
1077
+ const { railFits, openRail } = intelligence;
1082
1078
  useEffect(() => {
1083
1079
  if (!railFits) return;
1084
1080
  const id = selectedThread?.messageId ?? null;
@@ -1086,31 +1082,14 @@ function MailboxReading() {
1086
1082
  if (autoOpenedForRef.current === id) return;
1087
1083
  if (!selectedThread?.authenticity?.dkimMismatch) return;
1088
1084
  autoOpenedForRef.current = id;
1089
- if (!intelligenceOpen) onToggleIntelligence();
1085
+ openRail();
1090
1086
  }, [
1091
1087
  railFits,
1088
+ openRail,
1092
1089
  selectedThread?.messageId,
1093
1090
  selectedThread?.authenticity?.dkimMismatch,
1094
- intelligenceOpen,
1095
- onToggleIntelligence,
1096
1091
  ]);
1097
1092
 
1098
- const drawer = useIntelligenceDrawer(conversation?.threadId ?? null);
1099
- const drawerOpen = !railFits && drawer.isOpen;
1100
- const closeIntelligenceDrawer = drawer.close;
1101
- const openIntelligence = railFits ? onToggleIntelligence : drawer.open;
1102
- // The toolbar's control, which toggles whichever surface this width has.
1103
- const { toggle: toggleDrawer } = drawer;
1104
- const toggleIntelligence = useCallback(() => {
1105
- if (railFits) {
1106
- onToggleIntelligence();
1107
- return;
1108
- }
1109
- toggleDrawer();
1110
- }, [railFits, onToggleIntelligence, toggleDrawer]);
1111
- const intelligenceShowing =
1112
- hasThread && (railFits ? intelligenceOpen : drawerOpen);
1113
-
1114
1093
  const detailPane = conversation ? (
1115
1094
  <ConversationView
1116
1095
  threadId={conversation.threadId}
@@ -1118,9 +1097,7 @@ function MailboxReading() {
1118
1097
  subject={conversation.subject}
1119
1098
  selectedMessageId={conversation.messageId}
1120
1099
  authenticity={conversation.authenticity}
1121
- onOpenIntelligence={
1122
- conversation.authenticity?.dkimMismatch ? openIntelligence : undefined
1123
- }
1100
+ onOpenIntelligence={intelligence.open}
1124
1101
  />
1125
1102
  ) : (
1126
1103
  <ReadingPaneEmpty />
@@ -1132,9 +1109,9 @@ function MailboxReading() {
1132
1109
  <MessageToolbar
1133
1110
  hasThread={hasThread}
1134
1111
  messageId={conversation?.messageId}
1135
- intelligenceOpen={intelligenceShowing}
1136
- canToggleIntelligence={hasThread}
1137
- onToggleIntelligence={toggleIntelligence}
1112
+ intelligenceOpen={intelligence.isShowing}
1113
+ canToggleIntelligence={intelligence.canToggle}
1114
+ onToggleIntelligence={intelligence.toggle}
1138
1115
  onReply={onReply ? () => onReply("reply") : undefined}
1139
1116
  onReplyAll={onReply ? () => onReply("reply-all") : undefined}
1140
1117
  onForward={onReply ? () => onReply("forward") : undefined}
@@ -1154,21 +1131,14 @@ function MailboxReading() {
1154
1131
  />
1155
1132
  <div className="min-h-0 flex-1 overflow-hidden">{detailPane}</div>
1156
1133
  </section>
1157
- <Drawer
1158
- isOpen={drawerOpen}
1159
- onClose={closeIntelligenceDrawer}
1160
- ariaLabel="Message details"
1161
- side="right"
1162
- >
1163
- <IntelligencePane
1164
- onClose={closeIntelligenceDrawer}
1165
- thread={selectedThread}
1166
- mailboxId={mailboxId}
1167
- accountId={mailboxAccountId}
1168
- hideCloseButton
1169
- onAfterOptimisticRemove={handleDeselectIfRemoved}
1170
- />
1171
- </Drawer>
1134
+ <IntelligenceDrawer
1135
+ isOpen={intelligence.drawerOpen}
1136
+ onClose={intelligence.closeDrawer}
1137
+ thread={selectedThread}
1138
+ mailboxId={mailboxId}
1139
+ accountId={mailboxAccountId}
1140
+ onAfterOptimisticRemove={handleDeselectIfRemoved}
1141
+ />
1172
1142
  </>
1173
1143
  );
1174
1144
  }
@@ -1232,21 +1202,14 @@ function MailboxPhone() {
1232
1202
  }
1233
1203
  mobileIntelligenceOpen={drawer.isOpen}
1234
1204
  />
1235
- <Drawer
1205
+ <IntelligenceDrawer
1236
1206
  isOpen={drawer.isOpen}
1237
1207
  onClose={drawer.close}
1238
- ariaLabel="Message details"
1239
- side="right"
1240
- >
1241
- <IntelligencePane
1242
- onClose={drawer.close}
1243
- thread={selectedThread}
1244
- mailboxId={mailboxId}
1245
- accountId={mailboxAccountId}
1246
- hideCloseButton
1247
- onAfterOptimisticRemove={handleDeselectIfRemoved}
1248
- />
1249
- </Drawer>
1208
+ thread={selectedThread}
1209
+ mailboxId={mailboxId}
1210
+ accountId={mailboxAccountId}
1211
+ onAfterOptimisticRemove={handleDeselectIfRemoved}
1212
+ />
1250
1213
  </>
1251
1214
  );
1252
1215
  }
@@ -9,7 +9,8 @@ import { AuthenticityBanner } from "@/components/mail/AuthenticityBanner";
9
9
  *
10
10
  * Between 1024 and 1280px the shell has room for the reading pane but not the
11
11
  * intelligence rail, so the link opens the same right-anchored drawer the phone
12
- * uses `MailboxPane.Reading` mounts it whenever the rail does not fit. The
12
+ * uses. Every list pane mounts it wherever the rail does not fit — the mailbox,
13
+ * the brief and Flagged all read that from `useIntelligenceSurface` (#817). The
13
14
  * warning itself reads as one sentence: the link sits in the text flow at the
14
15
  * body's own size rather than floating off to the right as chrome.
15
16
  */
@@ -73,8 +74,8 @@ const intelligence: IntelligenceData = {
73
74
  };
74
75
 
75
76
  /**
76
- * The reading pane as `MailboxPane.Reading` composes it at this width: the
77
- * banner above the thread body, and the drawer it opens over the panes.
77
+ * The reading pane as every list pane composes it at this width: the banner
78
+ * above the thread body, and the drawer it opens over the panes.
78
79
  */
79
80
  const TwoPaneReading = () => {
80
81
  const [drawerOpen, setDrawerOpen] = useState(false);