@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 +1 -1
- package/src/components/compose/ComposeForm.tsx +18 -0
- package/src/components/compose/ConversationCompose.tsx +13 -1
- package/src/components/mail/BriefPane.tsx +63 -57
- package/src/components/mail/ConversationView.tsx +7 -12
- package/src/components/mail/FlaggedPane.tsx +63 -57
- package/src/components/mail/IntelligenceDrawer.tsx +44 -0
- package/src/components/mail/MailboxPane.tsx +24 -61
- package/src/components/mail/authenticity-warning.stories.tsx +4 -3
- package/src/components/mail/conversation-reply-identity.render.test.ts +304 -0
- package/src/components/mail/intelligence-surface-reach.render.test.ts +243 -0
- package/src/hooks/search-mirror-convergence.render.test.ts +197 -0
- package/src/hooks/search-mirror-detail.render.test.ts +4 -7
- package/src/hooks/useIntelligenceSurface.ts +64 -0
- package/src/hooks/useSearchField.render.test.ts +216 -0
- package/src/hooks/useSearchField.ts +85 -0
- package/src/hooks/useSearchMirror.ts +23 -10
- package/src/lib/mail-route.test.ts +69 -48
- package/src/lib/mail-route.ts +35 -13
- package/src/lib/search-view.test.ts +38 -27
- package/src/lib/search-view.ts +21 -2
- package/src/routes/mail.tsx +10 -44
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
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={
|
|
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
|
|
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 {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
332
|
+
const {
|
|
333
|
+
conversation,
|
|
334
|
+
selectedThread,
|
|
335
|
+
actions,
|
|
336
|
+
onReply,
|
|
337
|
+
handleDeselectIfRemoved,
|
|
338
|
+
} = useBriefPane();
|
|
337
339
|
const hasThread = Boolean(conversation);
|
|
338
|
-
const
|
|
340
|
+
const intelligence = useIntelligenceSurface(conversation?.threadId);
|
|
339
341
|
|
|
340
342
|
return (
|
|
341
|
-
|
|
342
|
-
<
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
444
|
+
<IntelligenceDrawer
|
|
432
445
|
isOpen={drawer.isOpen}
|
|
433
446
|
onClose={drawer.close}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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:
|
|
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
|
|
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 {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
335
|
+
const {
|
|
336
|
+
conversation,
|
|
337
|
+
selectedThread,
|
|
338
|
+
actions,
|
|
339
|
+
onReply,
|
|
340
|
+
handleDeselectIfRemoved,
|
|
341
|
+
} = useFlaggedPane();
|
|
340
342
|
const hasThread = Boolean(conversation);
|
|
341
|
-
const
|
|
343
|
+
const intelligence = useIntelligenceSurface(conversation?.threadId);
|
|
342
344
|
|
|
343
345
|
return (
|
|
344
|
-
|
|
345
|
-
<
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
445
|
+
<IntelligenceDrawer
|
|
433
446
|
isOpen={drawer.isOpen}
|
|
434
447
|
onClose={drawer.close}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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={
|
|
1136
|
-
canToggleIntelligence={
|
|
1137
|
-
onToggleIntelligence={
|
|
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
|
-
<
|
|
1158
|
-
isOpen={drawerOpen}
|
|
1159
|
-
onClose={
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
<
|
|
1205
|
+
<IntelligenceDrawer
|
|
1236
1206
|
isOpen={drawer.isOpen}
|
|
1237
1207
|
onClose={drawer.close}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
|
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
|
|
77
|
-
*
|
|
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);
|