@remit/web-client 0.0.161 → 0.0.163
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 +9 -0
- package/src/components/compose/InlineCompose.tsx +9 -9
- package/src/components/mail/BriefPane.tsx +11 -2
- package/src/components/mail/ConversationView.tsx +51 -55
- package/src/components/mail/DraftsView.tsx +2 -0
- package/src/components/mail/FlaggedPane.tsx +11 -2
- package/src/components/mail/MailboxPane.tsx +10 -28
- package/src/components/mail/MessageActionMenu.tsx +2 -1
- package/src/components/mail/MessageBody.tsx +2 -2
- package/src/components/mail/MessageCard.tsx +1 -4
- package/src/components/mail/MessageList.tsx +2 -0
- package/src/components/mail/OutboxPane.tsx +7 -1
- package/src/components/mail/SwipeableMessageRow.tsx +2 -0
- package/src/components/mail/conversation-reply-reach.render.test.ts +147 -12
- package/src/components/ui/KeyboardShortcutsModal.tsx +7 -2
- package/src/hooks/search-mirror-detail.render.test.ts +0 -1
- package/src/hooks/useSearchMirror.ts +11 -3
- package/src/lib/intelligence-pref.test.ts +77 -0
- package/src/lib/intelligence-pref.ts +33 -1
- package/src/lib/mail-context.ts +6 -6
- package/src/routes/mail.tsx +74 -30
- package/src/routing/fragment.test.ts +68 -0
- package/src/routing/fragment.ts +79 -16
- package/src/routing/index.ts +8 -2
- package/src/routing/nav-link.tsx +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.163",
|
|
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": {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
ComposeFormShell,
|
|
15
15
|
ComposeHeader,
|
|
16
16
|
type ComposeSendState,
|
|
17
|
+
type ComposeShellLayout,
|
|
17
18
|
ComposeSubjectField,
|
|
18
19
|
composeHeaderSummary,
|
|
19
20
|
defaultComposeLanguages,
|
|
@@ -67,6 +68,12 @@ interface ComposeFormProps {
|
|
|
67
68
|
sourceMessage?: RemitImapDescribeMessageResponse;
|
|
68
69
|
onClose: () => void;
|
|
69
70
|
onAccountChange?: (account: RemitImapAccountResponse) => void;
|
|
71
|
+
/**
|
|
72
|
+
* How the surface takes its height. A window in its own pane fills that
|
|
73
|
+
* pane; one opened as a block of the conversation grows with what is written
|
|
74
|
+
* in it and leaves the pane the only scroller.
|
|
75
|
+
*/
|
|
76
|
+
layout?: ComposeShellLayout;
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
const escapeHtml = (text: string): string =>
|
|
@@ -301,6 +308,7 @@ export const ComposeForm = ({
|
|
|
301
308
|
sourceMessage,
|
|
302
309
|
onClose,
|
|
303
310
|
onAccountChange,
|
|
311
|
+
layout = "fill",
|
|
304
312
|
}: ComposeFormProps) => {
|
|
305
313
|
const { state, setOutboxMessageId, startSendPolling } = useCompose();
|
|
306
314
|
const { pushError } = useErrorBanners();
|
|
@@ -751,6 +759,7 @@ export const ComposeForm = ({
|
|
|
751
759
|
|
|
752
760
|
return (
|
|
753
761
|
<ComposeFormShell
|
|
762
|
+
layout={layout}
|
|
754
763
|
banner={
|
|
755
764
|
selectedAccount && selectedAccountMissingSmtp ? (
|
|
756
765
|
<ComposeSmtpMissingBanner
|
|
@@ -12,22 +12,22 @@ interface InlineComposeProps {
|
|
|
12
12
|
onClose: () => void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The reply, as the top block of the conversation it answers. It takes no
|
|
17
|
+
* height of its own: it is as tall as what has been written in it and pushes
|
|
18
|
+
* the thread down as that grows, so the pane keeps the one scrollbar it had
|
|
19
|
+
* before the reply opened. A height here would give the pane a second one, with
|
|
20
|
+
* the caret in the inner track.
|
|
21
|
+
*/
|
|
15
22
|
export const InlineCompose = ({
|
|
16
23
|
mode,
|
|
17
24
|
account,
|
|
18
25
|
sourceMessage,
|
|
19
26
|
onClose,
|
|
20
27
|
}: InlineComposeProps) => (
|
|
21
|
-
|
|
22
|
-
// in landscape, or one with the keyboard up — a flat 400px is taller than
|
|
23
|
-
// the pane, and the surface's own header scrolls off before its verbs do.
|
|
24
|
-
//
|
|
25
|
-
// A desktop pane has room a phone does not, so the surface takes a height
|
|
26
|
-
// there rather than a ceiling. Left to size itself, the recipient rows and
|
|
27
|
-
// the verbs come first and the writing area gets its 120px floor — a couple
|
|
28
|
-
// of lines to answer a message in.
|
|
29
|
-
<div className="border-t border-line bg-canvas max-h-[min(400px,60dvh)] flex flex-col lg:h-[min(560px,55dvh)] lg:max-h-none">
|
|
28
|
+
<div className="border-b border-line bg-canvas">
|
|
30
29
|
<ComposeForm
|
|
30
|
+
layout="flow"
|
|
31
31
|
mode={mode}
|
|
32
32
|
account={account}
|
|
33
33
|
sourceMessage={sourceMessage}
|
|
@@ -41,7 +41,11 @@ import {
|
|
|
41
41
|
} from "@/hooks/useTriageLayer";
|
|
42
42
|
import type { ConversationTarget } from "@/lib/conversation-target";
|
|
43
43
|
import { useMailContext } from "@/lib/mail-context";
|
|
44
|
-
import
|
|
44
|
+
import {
|
|
45
|
+
type OpenThreadPath,
|
|
46
|
+
type OpenThreadTarget,
|
|
47
|
+
retainOpenPanels,
|
|
48
|
+
} from "@/routing";
|
|
45
49
|
|
|
46
50
|
/* ------------------------------------------------------------------ */
|
|
47
51
|
/* Context */
|
|
@@ -148,13 +152,18 @@ function BriefPaneProvider({ thread, children }: BriefPaneProps) {
|
|
|
148
152
|
// `searchInput`: a row can be tapped before the debounce settles, when
|
|
149
153
|
// the committed query is still empty.
|
|
150
154
|
search: (prev) => ({ ...prev, q: searchInput || undefined }),
|
|
155
|
+
hash: retainOpenPanels,
|
|
151
156
|
});
|
|
152
157
|
},
|
|
153
158
|
[navigate, searchInput],
|
|
154
159
|
);
|
|
155
160
|
|
|
156
161
|
const handleCloseThread = useCallback(() => {
|
|
157
|
-
navigate({
|
|
162
|
+
navigate({
|
|
163
|
+
to: "/mail/brief",
|
|
164
|
+
search: (prev) => prev,
|
|
165
|
+
hash: retainOpenPanels,
|
|
166
|
+
});
|
|
158
167
|
}, [navigate]);
|
|
159
168
|
|
|
160
169
|
const handleDeselectIfRemoved = useCallback(
|
|
@@ -122,8 +122,9 @@ export const ConversationView = ({
|
|
|
122
122
|
error,
|
|
123
123
|
refetch,
|
|
124
124
|
} = useQuery({
|
|
125
|
-
// The API reads a conversation oldest first (#81) and
|
|
126
|
-
//
|
|
125
|
+
// The API reads a conversation oldest first (#81) and no `order` is asked
|
|
126
|
+
// for: which turn came first is the thread's own fact, and which end of it
|
|
127
|
+
// the pane puts at the top is the pane's.
|
|
127
128
|
...threadDetailOperationsListThreadMessagesOptions({
|
|
128
129
|
path: { threadId },
|
|
129
130
|
}),
|
|
@@ -134,6 +135,11 @@ export const ConversationView = ({
|
|
|
134
135
|
[messagesResponse?.items],
|
|
135
136
|
);
|
|
136
137
|
const latestMessage = messages[messages.length - 1];
|
|
138
|
+
|
|
139
|
+
// Newest first, under the reply that answers it. The turn a reader came for
|
|
140
|
+
// is the last one, and reading it should not cost a scroll past everything
|
|
141
|
+
// that led to it.
|
|
142
|
+
const orderedMessages = useMemo(() => [...messages].reverse(), [messages]);
|
|
137
143
|
const messageRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
|
138
144
|
|
|
139
145
|
// Track which messages are expanded
|
|
@@ -158,7 +164,7 @@ export const ConversationView = ({
|
|
|
158
164
|
if (selected) initialExpanded.add(selected.threadMessageId);
|
|
159
165
|
}
|
|
160
166
|
setExpandedIds(initialExpanded);
|
|
161
|
-
setFocusedIndex(
|
|
167
|
+
setFocusedIndex(0);
|
|
162
168
|
}
|
|
163
169
|
}, [threadId, messages, currentThreadId, selectedMessageId]);
|
|
164
170
|
|
|
@@ -174,38 +180,39 @@ export const ConversationView = ({
|
|
|
174
180
|
});
|
|
175
181
|
}, []);
|
|
176
182
|
|
|
177
|
-
// Scroll focused message into view
|
|
183
|
+
// Scroll focused message into view. j and k walk the pane as it reads, so
|
|
184
|
+
// the index they move is over the displayed order, not the fetched one.
|
|
178
185
|
const scrollToMessage = useCallback(
|
|
179
186
|
(index: number) => {
|
|
180
|
-
const message =
|
|
187
|
+
const message = orderedMessages[index];
|
|
181
188
|
if (!message) return;
|
|
182
189
|
const element = messageRefs.current.get(message.threadMessageId);
|
|
183
190
|
element?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
184
191
|
},
|
|
185
|
-
[
|
|
192
|
+
[orderedMessages],
|
|
186
193
|
);
|
|
187
194
|
|
|
188
195
|
// Keyboard navigation handlers
|
|
189
196
|
const focusNext = useCallback(() => {
|
|
190
|
-
if (
|
|
191
|
-
const nextIndex = Math.min(focusedIndex + 1,
|
|
197
|
+
if (orderedMessages.length === 0) return;
|
|
198
|
+
const nextIndex = Math.min(focusedIndex + 1, orderedMessages.length - 1);
|
|
192
199
|
setFocusedIndex(nextIndex);
|
|
193
200
|
scrollToMessage(nextIndex);
|
|
194
|
-
}, [
|
|
201
|
+
}, [orderedMessages.length, focusedIndex, scrollToMessage]);
|
|
195
202
|
|
|
196
203
|
const focusPrevious = useCallback(() => {
|
|
197
|
-
if (
|
|
204
|
+
if (orderedMessages.length === 0) return;
|
|
198
205
|
const prevIndex = Math.max(focusedIndex - 1, 0);
|
|
199
206
|
setFocusedIndex(prevIndex);
|
|
200
207
|
scrollToMessage(prevIndex);
|
|
201
|
-
}, [
|
|
208
|
+
}, [orderedMessages.length, focusedIndex, scrollToMessage]);
|
|
202
209
|
|
|
203
210
|
const toggleFocusedMessage = useCallback(() => {
|
|
204
|
-
const message =
|
|
211
|
+
const message = orderedMessages[focusedIndex];
|
|
205
212
|
if (message) {
|
|
206
213
|
toggleExpanded(message.threadMessageId);
|
|
207
214
|
}
|
|
208
|
-
}, [
|
|
215
|
+
}, [orderedMessages, focusedIndex, toggleExpanded]);
|
|
209
216
|
|
|
210
217
|
const { data: config } = useQuery({
|
|
211
218
|
...configOperationsGetConfigOptions(),
|
|
@@ -282,25 +289,18 @@ export const ConversationView = ({
|
|
|
282
289
|
onComposeClose?.();
|
|
283
290
|
}, [onComposeClose]);
|
|
284
291
|
|
|
285
|
-
// Compose opens
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// been fetched and grows when that lands, so a single scroll at open time
|
|
291
|
-
// puts whatever it grows past back below the fold.
|
|
292
|
+
// Compose opens at the head of the pane, which from halfway down a long
|
|
293
|
+
// thread is several screens up: without this, replying looks like nothing
|
|
294
|
+
// happening. Aligned on its top edge, where the recipients are, and left
|
|
295
|
+
// alone afterwards — it grows downward as it is written into, so following
|
|
296
|
+
// its height would pull the rows being typed into off the top.
|
|
292
297
|
const composeRef = useRef<HTMLDivElement>(null);
|
|
293
298
|
useEffect(() => {
|
|
294
299
|
if (composeMode === null || composeOpenings === 0) return;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
reveal();
|
|
300
|
-
if (typeof ResizeObserver === "undefined") return;
|
|
301
|
-
const observer = new ResizeObserver(reveal);
|
|
302
|
-
observer.observe(surface);
|
|
303
|
-
return () => observer.disconnect();
|
|
300
|
+
composeRef.current?.scrollIntoView({
|
|
301
|
+
behavior: "smooth",
|
|
302
|
+
block: "start",
|
|
303
|
+
});
|
|
304
304
|
}, [composeOpenings, composeMode]);
|
|
305
305
|
|
|
306
306
|
// Register keyboard shortcuts
|
|
@@ -357,8 +357,8 @@ export const ConversationView = ({
|
|
|
357
357
|
// wired whatever the account's SMTP state: compose is where an account that
|
|
358
358
|
// cannot send says so.
|
|
359
359
|
const renderMessages = (mobile: boolean) => (
|
|
360
|
-
<div>
|
|
361
|
-
{
|
|
360
|
+
<div data-testid="conversation-messages">
|
|
361
|
+
{orderedMessages.map((message, index) => (
|
|
362
362
|
<div
|
|
363
363
|
key={message.threadMessageId}
|
|
364
364
|
ref={(el) => {
|
|
@@ -385,6 +385,21 @@ export const ConversationView = ({
|
|
|
385
385
|
</div>
|
|
386
386
|
);
|
|
387
387
|
|
|
388
|
+
// The reply leads the pane, above the turn it answers: what is being written
|
|
389
|
+
// is what the reader came back for, and it is not something to go looking
|
|
390
|
+
// for under a thread. It scrolls with the conversation rather than floating
|
|
391
|
+
// over it, so the pane has one scrollbar whether or not a reply is open.
|
|
392
|
+
const compose = composeMode !== null && (
|
|
393
|
+
<div ref={composeRef}>
|
|
394
|
+
<InlineCompose
|
|
395
|
+
mode={composeMode}
|
|
396
|
+
account={activeAccount}
|
|
397
|
+
sourceMessage={latestMessageData}
|
|
398
|
+
onClose={handleCloseCompose}
|
|
399
|
+
/>
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
402
|
+
|
|
388
403
|
// Subject block: matches the AppShell ReadingPane reference exactly —
|
|
389
404
|
// px-5 pt-5 pb-3, text-lg leading-snug, 2xs count. Subject scrolls
|
|
390
405
|
// with the thread body; no subject in the toolbar chrome.
|
|
@@ -419,17 +434,8 @@ export const ConversationView = ({
|
|
|
419
434
|
onOpenIntelligence={onOpenIntelligence}
|
|
420
435
|
/>
|
|
421
436
|
)}
|
|
437
|
+
{compose}
|
|
422
438
|
{renderMessages(true)}
|
|
423
|
-
{composeMode !== null && (
|
|
424
|
-
<div ref={composeRef}>
|
|
425
|
-
<InlineCompose
|
|
426
|
-
mode={composeMode}
|
|
427
|
-
account={activeAccount}
|
|
428
|
-
sourceMessage={latestMessageData}
|
|
429
|
-
onClose={handleCloseCompose}
|
|
430
|
-
/>
|
|
431
|
-
</div>
|
|
432
|
-
)}
|
|
433
439
|
</MobileReadingPane>
|
|
434
440
|
);
|
|
435
441
|
}
|
|
@@ -443,22 +449,12 @@ export const ConversationView = ({
|
|
|
443
449
|
onOpenIntelligence={onOpenIntelligence}
|
|
444
450
|
/>
|
|
445
451
|
)}
|
|
446
|
-
{/* The
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
reach it. */}
|
|
452
|
+
{/* The one scroller in the pane. The reply and the thread are both in
|
|
453
|
+
it, so neither has a scrollbar of its own and neither is squeezed
|
|
454
|
+
by the height the other takes. */}
|
|
450
455
|
<div className="min-h-0 flex-1 overflow-auto">
|
|
456
|
+
{compose}
|
|
451
457
|
{renderMessages(false)}
|
|
452
|
-
{composeMode !== null && (
|
|
453
|
-
<div ref={composeRef}>
|
|
454
|
-
<InlineCompose
|
|
455
|
-
mode={composeMode}
|
|
456
|
-
account={activeAccount}
|
|
457
|
-
sourceMessage={latestMessageData}
|
|
458
|
-
onClose={handleCloseCompose}
|
|
459
|
-
/>
|
|
460
|
-
</div>
|
|
461
|
-
)}
|
|
462
458
|
</div>
|
|
463
459
|
</article>
|
|
464
460
|
);
|
|
@@ -41,6 +41,7 @@ import { FileText, Inbox, Trash2 } from "lucide-react";
|
|
|
41
41
|
import { useMemo } from "react";
|
|
42
42
|
import { useCompose } from "@/components/compose/ComposeProvider";
|
|
43
43
|
import { groupDraftSections } from "@/lib/drafts";
|
|
44
|
+
import { retainOpenPanels } from "@/routing";
|
|
44
45
|
import { NavMenuButton } from "./NavMenuButton";
|
|
45
46
|
|
|
46
47
|
// ---------------------------------------------------------------------------
|
|
@@ -197,6 +198,7 @@ export function DraftsView({
|
|
|
197
198
|
to: "/mail/$mailboxId/$threadId/$messageId",
|
|
198
199
|
params: { mailboxId, threadId, messageId },
|
|
199
200
|
search: (prev) => prev,
|
|
201
|
+
hash: retainOpenPanels,
|
|
200
202
|
});
|
|
201
203
|
};
|
|
202
204
|
|
|
@@ -52,7 +52,11 @@ import {
|
|
|
52
52
|
} from "@/hooks/useTriageLayer";
|
|
53
53
|
import type { ConversationTarget } from "@/lib/conversation-target";
|
|
54
54
|
import { useMailContext } from "@/lib/mail-context";
|
|
55
|
-
import
|
|
55
|
+
import {
|
|
56
|
+
type OpenThreadPath,
|
|
57
|
+
type OpenThreadTarget,
|
|
58
|
+
retainOpenPanels,
|
|
59
|
+
} from "@/routing";
|
|
56
60
|
|
|
57
61
|
/* ------------------------------------------------------------------ */
|
|
58
62
|
/* Context */
|
|
@@ -154,13 +158,18 @@ function FlaggedPaneProvider({ thread, children }: FlaggedPaneProps) {
|
|
|
154
158
|
// `searchInput`: a row can be tapped before the debounce settles, when
|
|
155
159
|
// the committed query is still empty.
|
|
156
160
|
search: (prev) => ({ ...prev, q: searchInput || undefined }),
|
|
161
|
+
hash: retainOpenPanels,
|
|
157
162
|
});
|
|
158
163
|
},
|
|
159
164
|
[navigate, searchInput],
|
|
160
165
|
);
|
|
161
166
|
|
|
162
167
|
const handleCloseThread = useCallback(() => {
|
|
163
|
-
navigate({
|
|
168
|
+
navigate({
|
|
169
|
+
to: "/mail/flagged",
|
|
170
|
+
search: (prev) => prev,
|
|
171
|
+
hash: retainOpenPanels,
|
|
172
|
+
});
|
|
164
173
|
}, [navigate]);
|
|
165
174
|
|
|
166
175
|
const handleDeselectIfRemoved = useCallback(
|
|
@@ -108,7 +108,6 @@ import {
|
|
|
108
108
|
inboxFilterParams,
|
|
109
109
|
sameInboxFilter,
|
|
110
110
|
} from "@/lib/inbox-filters";
|
|
111
|
-
import { readIntelligencePref } from "@/lib/intelligence-pref";
|
|
112
111
|
import { junkDestination } from "@/lib/junk-destination";
|
|
113
112
|
import { useMailContext } from "@/lib/mail-context";
|
|
114
113
|
import { useMailFreshness } from "@/lib/mail-freshness";
|
|
@@ -125,7 +124,11 @@ import {
|
|
|
125
124
|
applyResidualTokens,
|
|
126
125
|
threadSearchTokens,
|
|
127
126
|
} from "@/lib/thread-search-tokens";
|
|
128
|
-
import
|
|
127
|
+
import {
|
|
128
|
+
type OpenThreadPath,
|
|
129
|
+
type OpenThreadTarget,
|
|
130
|
+
retainOpenPanels,
|
|
131
|
+
} from "@/routing";
|
|
129
132
|
import { MailViewChrome } from "./MailViewChrome";
|
|
130
133
|
|
|
131
134
|
/* ------------------------------------------------------------------ */
|
|
@@ -263,16 +266,9 @@ function MailboxPaneProvider({
|
|
|
263
266
|
const navigate = useNavigate();
|
|
264
267
|
const threadId = thread?.threadId;
|
|
265
268
|
const pointedAtMessageId = thread?.messageId;
|
|
266
|
-
const tier = useLayoutTier();
|
|
267
|
-
const isDesktop = tier === "desktop";
|
|
268
269
|
const telemetry = useTelemetry();
|
|
269
|
-
const {
|
|
270
|
-
|
|
271
|
-
searchQuery,
|
|
272
|
-
intelligenceOpen,
|
|
273
|
-
onToggleIntelligence,
|
|
274
|
-
onSetIntelligenceOpen,
|
|
275
|
-
} = useMailContext();
|
|
270
|
+
const { accounts, searchQuery, intelligenceOpen, onToggleIntelligence } =
|
|
271
|
+
useMailContext();
|
|
276
272
|
const tokenContext = useSearchTokenContext();
|
|
277
273
|
|
|
278
274
|
const normalizedSearchQuery = normalizeSearchQuery(searchQuery);
|
|
@@ -471,6 +467,7 @@ function MailboxPaneProvider({
|
|
|
471
467
|
to: "/mail/$mailboxId",
|
|
472
468
|
params: { mailboxId },
|
|
473
469
|
search: (prev) => prev,
|
|
470
|
+
hash: retainOpenPanels,
|
|
474
471
|
});
|
|
475
472
|
}, [mailboxId, navigate]);
|
|
476
473
|
|
|
@@ -480,6 +477,7 @@ function MailboxPaneProvider({
|
|
|
480
477
|
to: "/mail/$mailboxId/$threadId/$messageId",
|
|
481
478
|
params: { mailboxId, ...target },
|
|
482
479
|
search: (prev) => prev,
|
|
480
|
+
hash: retainOpenPanels,
|
|
483
481
|
});
|
|
484
482
|
},
|
|
485
483
|
[mailboxId, navigate],
|
|
@@ -535,23 +533,6 @@ function MailboxPaneProvider({
|
|
|
535
533
|
onToggleIntelligence,
|
|
536
534
|
]);
|
|
537
535
|
|
|
538
|
-
// Desktop default-open (#782)
|
|
539
|
-
const appliedDefaultRef = useRef(false);
|
|
540
|
-
useEffect(() => {
|
|
541
|
-
if (appliedDefaultRef.current) return;
|
|
542
|
-
if (!isDesktop) return;
|
|
543
|
-
if (!selectedThread?.messageId) return;
|
|
544
|
-
appliedDefaultRef.current = true;
|
|
545
|
-
if (readIntelligencePref() && !intelligenceOpen) {
|
|
546
|
-
onSetIntelligenceOpen(true);
|
|
547
|
-
}
|
|
548
|
-
}, [
|
|
549
|
-
isDesktop,
|
|
550
|
-
selectedThread?.messageId,
|
|
551
|
-
intelligenceOpen,
|
|
552
|
-
onSetIntelligenceOpen,
|
|
553
|
-
]);
|
|
554
|
-
|
|
555
536
|
// The mailbox's own unseen total. A count over the loaded pages undercounts
|
|
556
537
|
// every mailbox larger than one page and creeps upward as the user scrolls,
|
|
557
538
|
// so there is no fallback: until the mailbox resolves there is no number.
|
|
@@ -1048,6 +1029,7 @@ function MailboxList() {
|
|
|
1048
1029
|
// `searchInput`: a row can be tapped before the debounce settles, when
|
|
1049
1030
|
// the committed query is still empty.
|
|
1050
1031
|
search: (prev) => ({ ...prev, q: searchInput || undefined }),
|
|
1032
|
+
hash: retainOpenPanels,
|
|
1051
1033
|
});
|
|
1052
1034
|
},
|
|
1053
1035
|
[mailboxId, navigate, searchInput, threads],
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
type ThreadListSnapshotEntry,
|
|
36
36
|
threadListCacheKeys,
|
|
37
37
|
} from "@/lib/thread-list-cache";
|
|
38
|
-
import { useOpenThreadPath } from "@/routing";
|
|
38
|
+
import { retainOpenPanels, useOpenThreadPath } from "@/routing";
|
|
39
39
|
import { MoveToTrigger } from "./MoveToTrigger";
|
|
40
40
|
|
|
41
41
|
interface ThreadMessagesData {
|
|
@@ -193,6 +193,7 @@ export const MessageActionMenu = ({
|
|
|
193
193
|
to: "/mail/$mailboxId",
|
|
194
194
|
params: { mailboxId },
|
|
195
195
|
search: (prev) => prev,
|
|
196
|
+
hash: retainOpenPanels,
|
|
196
197
|
});
|
|
197
198
|
},
|
|
198
199
|
[selectedMessageId, mailboxId, navigate],
|
|
@@ -68,8 +68,8 @@ interface MessageBodyProps {
|
|
|
68
68
|
category?: EmailRenderCategory;
|
|
69
69
|
/**
|
|
70
70
|
* Extra classes for the `.message-body` wrapper. `MessageCard` leaves this
|
|
71
|
-
* unset: the kit's `ExpandedMessage`
|
|
72
|
-
*
|
|
71
|
+
* unset: the kit's `ExpandedMessage` owns the gutter, and runs the sandboxed
|
|
72
|
+
* email frame out of it so mail renders on its own ground.
|
|
73
73
|
*/
|
|
74
74
|
className?: string;
|
|
75
75
|
}
|
|
@@ -324,14 +324,11 @@ const ExpandedCard = ({
|
|
|
324
324
|
isTrusted={isTrusted}
|
|
325
325
|
category={toDisplayCategory(threadMessage.category)}
|
|
326
326
|
/>
|
|
327
|
-
{/* The body slot runs edge to edge on a phone; the attachment
|
|
328
|
-
list is app chrome, not part of the email, so it takes the
|
|
329
|
-
gutter back. */}
|
|
330
327
|
<MessageAttachments
|
|
331
328
|
messageId={threadMessage.messageId}
|
|
332
329
|
bodyParts={messageData?.bodyParts}
|
|
333
330
|
hasAttachment={threadMessage.hasAttachment}
|
|
334
|
-
className="mt-4
|
|
331
|
+
className="mt-4"
|
|
335
332
|
/>
|
|
336
333
|
</div>
|
|
337
334
|
)
|
|
@@ -53,6 +53,7 @@ import { listVerbRequest } from "@/lib/list-verb-request";
|
|
|
53
53
|
import { shouldExitSelectionOnNavigate } from "@/lib/selection-mode";
|
|
54
54
|
import { cn } from "@/lib/utils";
|
|
55
55
|
import { useSelectionWizard, useWizardStepValue } from "@/lib/wizard-history";
|
|
56
|
+
import { retainOpenPanels } from "@/routing";
|
|
56
57
|
import { LabelApplyTrigger } from "./LabelApplyTrigger";
|
|
57
58
|
import {
|
|
58
59
|
type EscalatedSelection,
|
|
@@ -634,6 +635,7 @@ export const MessageList = ({
|
|
|
634
635
|
to: "/mail/$mailboxId/$threadId/$messageId",
|
|
635
636
|
params: { mailboxId, threadId, messageId },
|
|
636
637
|
search: (prev) => prev,
|
|
638
|
+
hash: retainOpenPanels,
|
|
637
639
|
replace: options?.replace,
|
|
638
640
|
});
|
|
639
641
|
},
|
|
@@ -63,6 +63,7 @@ import { isOutboxListRow, isUnsendableStatus } from "@/lib/outbox-status";
|
|
|
63
63
|
import { normalizeSearchQuery } from "@/lib/search-query";
|
|
64
64
|
import { parseSearchTokens } from "@/lib/search-tokens";
|
|
65
65
|
import { cn } from "@/lib/utils";
|
|
66
|
+
import { retainOpenPanels } from "@/routing";
|
|
66
67
|
|
|
67
68
|
/* ------------------------------------------------------------------ */
|
|
68
69
|
/* Helpers (shared between List, Reading, Phone sub-views) */
|
|
@@ -218,13 +219,18 @@ function OutboxPaneProvider({
|
|
|
218
219
|
to: "/mail/outbox/draft/$outboxMessageId",
|
|
219
220
|
params: { outboxMessageId },
|
|
220
221
|
search: (prev) => prev,
|
|
222
|
+
hash: retainOpenPanels,
|
|
221
223
|
});
|
|
222
224
|
},
|
|
223
225
|
[navigate],
|
|
224
226
|
);
|
|
225
227
|
|
|
226
228
|
const handleCloseMessage = useCallback(() => {
|
|
227
|
-
navigate({
|
|
229
|
+
navigate({
|
|
230
|
+
to: "/mail/outbox",
|
|
231
|
+
search: (prev) => prev,
|
|
232
|
+
hash: retainOpenPanels,
|
|
233
|
+
});
|
|
228
234
|
}, [navigate]);
|
|
229
235
|
|
|
230
236
|
const ctx: OutboxPaneContextValue = {
|
|
@@ -10,6 +10,7 @@ import { useNavigate } from "@tanstack/react-router";
|
|
|
10
10
|
import { useCallback, useState } from "react";
|
|
11
11
|
import { toDisplayCategory } from "@/lib/display-category";
|
|
12
12
|
import { formatEmailDate } from "@/lib/format";
|
|
13
|
+
import { retainOpenPanels } from "@/routing";
|
|
13
14
|
import { MessageListItem } from "./MessageListItem";
|
|
14
15
|
import { useModifierSelect } from "./useModifierSelect";
|
|
15
16
|
|
|
@@ -105,6 +106,7 @@ export const SwipeableMessageRow = ({
|
|
|
105
106
|
messageId: thread.messageId,
|
|
106
107
|
},
|
|
107
108
|
search: (prev) => prev,
|
|
109
|
+
hash: retainOpenPanels,
|
|
108
110
|
});
|
|
109
111
|
}, [navigate, mailboxId, thread.threadId, thread.messageId]);
|
|
110
112
|
|