@remit/web-client 0.0.149 → 0.0.151
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/ComposeProvider.tsx +27 -5
- package/src/components/layout/ComposeFab.tsx +3 -2
- package/src/components/mail/BriefPane.tsx +8 -8
- package/src/components/mail/DailyBrief.tsx +3 -7
- package/src/components/mail/DraftsView.tsx +7 -3
- package/src/components/mail/IntelligencePane.tsx +4 -3
- package/src/components/mail/MailListHeader.tsx +1 -5
- package/src/components/mail/MailSidebarAdapter.tsx +4 -3
- package/src/components/mail/MailboxPane.tsx +130 -150
- package/src/components/mail/MessageActionMenu.tsx +12 -8
- package/src/components/mail/MessageList.selection.test.ts +6 -7
- package/src/components/mail/MessageList.tsx +28 -32
- package/src/components/mail/MessageListItem.tsx +2 -1
- package/src/components/mail/MessageRow.tsx +13 -10
- package/src/components/mail/OutboxPane.tsx +31 -28
- package/src/components/mail/SwipeableMessageRow.modifier-select.test.ts +8 -3
- package/src/components/mail/SwipeableMessageRow.tsx +8 -12
- package/src/hooks/useIntelligenceData.ts +1 -0
- package/src/lib/conversation-target.ts +7 -49
- package/src/lib/mail-search.ts +13 -11
- package/src/routeTree.gen.ts +96 -0
- package/src/routes/mail/$mailboxId/$threadId/$messageId.tsx +19 -0
- package/src/routes/mail/$mailboxId/$threadId/index.tsx +14 -0
- package/src/routes/mail/$mailboxId/$threadId.tsx +12 -0
- package/src/routes/mail/$mailboxId.tsx +7 -3
- package/src/routes/mail/brief.tsx +2 -2
- package/src/routes/mail/outbox/draft/$outboxMessageId.tsx +19 -0
- package/src/routes/mail/outbox.tsx +6 -1
- package/src/routing/index.ts +6 -5
- package/src/routing/open-thread.ts +58 -0
- package/src/routing/outbox-draft.ts +17 -0
- package/src/lib/conversation-target.test.ts +0 -68
- package/src/lib/search-pending.test.ts +0 -99
- package/src/lib/search-pending.ts +0 -49
- package/src/routing/brief-thread.ts +0 -47
|
@@ -4,26 +4,30 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The literal lists are declared as siblings and TanStack matches literals
|
|
6
6
|
* first, so a mailbox id can never be read as one of them.
|
|
7
|
+
*
|
|
8
|
+
* The open thread and the message inside it are the segments below, which is why
|
|
9
|
+
* nothing here reads a selection out of the query.
|
|
7
10
|
*/
|
|
8
11
|
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
|
9
12
|
import { MailShell } from "@/components/layout/MailShell";
|
|
10
13
|
import { MailboxPane } from "@/components/mail/MailboxPane";
|
|
11
14
|
import { useSearchMirror } from "@/hooks/useSearchMirror";
|
|
12
15
|
import { mailboxSearchSchema } from "@/lib/mail-search";
|
|
16
|
+
import { useOpenThreadPath } from "@/routing";
|
|
13
17
|
|
|
14
18
|
function MailboxLayout() {
|
|
15
19
|
const { mailboxId } = Route.useParams();
|
|
16
|
-
const
|
|
20
|
+
const thread = useOpenThreadPath();
|
|
17
21
|
useSearchMirror({ to: "/mail/$mailboxId", params: { mailboxId } });
|
|
18
22
|
|
|
19
23
|
return (
|
|
20
|
-
<MailboxPane mailboxId={mailboxId}
|
|
24
|
+
<MailboxPane mailboxId={mailboxId} thread={thread}>
|
|
21
25
|
<MailShell
|
|
22
26
|
phone={<MailboxPane.Phone />}
|
|
23
27
|
list={<MailboxPane.List />}
|
|
24
28
|
reading={<Outlet />}
|
|
25
29
|
intelligence={<MailboxPane.Intelligence />}
|
|
26
|
-
hasThread={Boolean(
|
|
30
|
+
hasThread={Boolean(thread)}
|
|
27
31
|
/>
|
|
28
32
|
</MailboxPane>
|
|
29
33
|
);
|
|
@@ -17,7 +17,7 @@ import { BriefPane } from "@/components/mail/BriefPane";
|
|
|
17
17
|
import { ErrorState } from "@/components/ui/ErrorState";
|
|
18
18
|
import { useSearchMirror } from "@/hooks/useSearchMirror";
|
|
19
19
|
import { briefSearchSchema } from "@/lib/mail-search";
|
|
20
|
-
import {
|
|
20
|
+
import { useOpenThreadPath } from "@/routing";
|
|
21
21
|
|
|
22
22
|
const BriefError = ({ error, reset }: ErrorComponentProps) => (
|
|
23
23
|
<div className="flex h-full items-center justify-center bg-canvas p-4">
|
|
@@ -30,7 +30,7 @@ const BriefError = ({ error, reset }: ErrorComponentProps) => (
|
|
|
30
30
|
);
|
|
31
31
|
|
|
32
32
|
function BriefLayout() {
|
|
33
|
-
const thread =
|
|
33
|
+
const thread = useOpenThreadPath();
|
|
34
34
|
useSearchMirror({ to: "/mail/brief" });
|
|
35
35
|
|
|
36
36
|
return (
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /mail/outbox/draft/$outboxMessageId — one outbox message open in the
|
|
3
|
+
* reading pane.
|
|
4
|
+
*
|
|
5
|
+
* An outbox message is addressable on its own: it belongs to no thread and no
|
|
6
|
+
* folder, so the id is the whole address. The `draft/` prefix keeps the id in a
|
|
7
|
+
* named segment, so the literal siblings the outbox will grow stay
|
|
8
|
+
* distinguishable from it.
|
|
9
|
+
*/
|
|
10
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
11
|
+
import { OutboxPane } from "@/components/mail/OutboxPane";
|
|
12
|
+
|
|
13
|
+
function OutboxDraftPane() {
|
|
14
|
+
return <OutboxPane.Reading />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Route = createFileRoute("/mail/outbox/draft/$outboxMessageId")({
|
|
18
|
+
component: OutboxDraftPane,
|
|
19
|
+
});
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* /mail/outbox — one of the four list layouts. Two panes, list and reading;
|
|
3
3
|
* there is no intelligence rail for mail that has not been sent yet.
|
|
4
|
+
*
|
|
5
|
+
* The open message is the segment below this one, which is why nothing here
|
|
6
|
+
* reads a selection out of the query.
|
|
4
7
|
*/
|
|
5
8
|
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
|
6
9
|
import { MailShell } from "@/components/layout/MailShell";
|
|
7
10
|
import { OutboxPane } from "@/components/mail/OutboxPane";
|
|
8
11
|
import { useSearchMirror } from "@/hooks/useSearchMirror";
|
|
9
12
|
import { outboxSearchSchema } from "@/lib/mail-search";
|
|
13
|
+
import { useOutboxDraftId } from "@/routing";
|
|
10
14
|
|
|
11
15
|
function OutboxLayout() {
|
|
16
|
+
const outboxMessageId = useOutboxDraftId();
|
|
12
17
|
useSearchMirror({ to: "/mail/outbox" });
|
|
13
18
|
|
|
14
19
|
return (
|
|
15
|
-
<OutboxPane>
|
|
20
|
+
<OutboxPane outboxMessageId={outboxMessageId}>
|
|
16
21
|
<MailShell
|
|
17
22
|
phone={<OutboxPane.Phone />}
|
|
18
23
|
list={<OutboxPane.List />}
|
package/src/routing/index.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
export {
|
|
2
|
-
type BriefThreadPath,
|
|
3
|
-
type BriefThreadTarget,
|
|
4
|
-
useBriefThreadPath,
|
|
5
|
-
} from "./brief-thread";
|
|
6
1
|
export {
|
|
7
2
|
type PanelFragment,
|
|
8
3
|
panelFragments,
|
|
@@ -10,3 +5,9 @@ export {
|
|
|
10
5
|
useOpenPanel,
|
|
11
6
|
} from "./fragment";
|
|
12
7
|
export { NavLink, type NavLinkProps } from "./nav-link";
|
|
8
|
+
export {
|
|
9
|
+
type OpenThreadPath,
|
|
10
|
+
type OpenThreadTarget,
|
|
11
|
+
useOpenThreadPath,
|
|
12
|
+
} from "./open-thread";
|
|
13
|
+
export { useOutboxDraftId } from "./outbox-draft";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useParams } from "@tanstack/react-router";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
|
|
4
|
+
/** The conversation a list has open, as the address states it. */
|
|
5
|
+
export interface OpenThreadPath {
|
|
6
|
+
threadId: string;
|
|
7
|
+
/**
|
|
8
|
+
* Which message inside the thread is expanded and scrolled to. Absent on a
|
|
9
|
+
* bare thread address, where the newest message answers for the conversation.
|
|
10
|
+
*/
|
|
11
|
+
messageId: string | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A conversation to open. The thread is what the pane fetches by; the message is
|
|
16
|
+
* the row the reader pointed at, so a list always knows both.
|
|
17
|
+
*/
|
|
18
|
+
export interface OpenThreadTarget {
|
|
19
|
+
threadId: string;
|
|
20
|
+
messageId: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The thread the address has open, whichever list is browsing it.
|
|
25
|
+
*
|
|
26
|
+
* The thread is a child route of the list, and the list layout mounts the pane
|
|
27
|
+
* above the `Outlet` — so it asks the router which of its children matched
|
|
28
|
+
* rather than reading a param it does not own. A path matches one list at a
|
|
29
|
+
* time, so one hook answers for every list rather than each list keeping its
|
|
30
|
+
* own copy of the same question. Each `from` names a real route, so a segment
|
|
31
|
+
* that does not exist fails to compile.
|
|
32
|
+
*/
|
|
33
|
+
export function useOpenThreadPath(): OpenThreadPath | undefined {
|
|
34
|
+
const briefThread = useParams({
|
|
35
|
+
from: "/mail/brief/$threadId",
|
|
36
|
+
shouldThrow: false,
|
|
37
|
+
});
|
|
38
|
+
const briefMessage = useParams({
|
|
39
|
+
from: "/mail/brief/$threadId/$messageId",
|
|
40
|
+
shouldThrow: false,
|
|
41
|
+
});
|
|
42
|
+
const mailboxThread = useParams({
|
|
43
|
+
from: "/mail/$mailboxId/$threadId",
|
|
44
|
+
shouldThrow: false,
|
|
45
|
+
});
|
|
46
|
+
const mailboxMessage = useParams({
|
|
47
|
+
from: "/mail/$mailboxId/$threadId/$messageId",
|
|
48
|
+
shouldThrow: false,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const threadId = briefThread?.threadId ?? mailboxThread?.threadId;
|
|
52
|
+
const messageId = briefMessage?.messageId ?? mailboxMessage?.messageId;
|
|
53
|
+
|
|
54
|
+
return useMemo(
|
|
55
|
+
() => (threadId ? { threadId, messageId } : undefined),
|
|
56
|
+
[threadId, messageId],
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useParams } from "@tanstack/react-router";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The outbox message the reader has open, read off the path.
|
|
5
|
+
*
|
|
6
|
+
* The message is a child route of the list, and the list layout mounts the
|
|
7
|
+
* panes above the `Outlet` — so it asks the router which of its children
|
|
8
|
+
* matched rather than reading a param it does not own. The `from` names a real
|
|
9
|
+
* route, so a segment that does not exist fails to compile.
|
|
10
|
+
*/
|
|
11
|
+
export function useOutboxDraftId(): string | undefined {
|
|
12
|
+
const draft = useParams({
|
|
13
|
+
from: "/mail/outbox/draft/$outboxMessageId",
|
|
14
|
+
shouldThrow: false,
|
|
15
|
+
});
|
|
16
|
+
return draft?.outboxMessageId;
|
|
17
|
+
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, test } from "node:test";
|
|
3
|
-
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
4
|
-
import { buildConversationTarget } from "./conversation-target.js";
|
|
5
|
-
|
|
6
|
-
function thread(
|
|
7
|
-
overrides: Partial<RemitImapThreadMessageResponse> = {},
|
|
8
|
-
): RemitImapThreadMessageResponse {
|
|
9
|
-
return {
|
|
10
|
-
messageId: "m1",
|
|
11
|
-
threadId: "t1",
|
|
12
|
-
mailboxId: "mb1",
|
|
13
|
-
subject: "Loaded subject",
|
|
14
|
-
isRead: false,
|
|
15
|
-
...overrides,
|
|
16
|
-
} as RemitImapThreadMessageResponse;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe("buildConversationTarget", () => {
|
|
20
|
-
test("prefers the loaded thread, carrying its display fields", () => {
|
|
21
|
-
const authenticity = { dkimMismatch: true, fromDomain: "example.com" };
|
|
22
|
-
const target = buildConversationTarget(thread({ authenticity }), {
|
|
23
|
-
messageId: "m1",
|
|
24
|
-
threadId: "ignored",
|
|
25
|
-
mailboxId: "ignored",
|
|
26
|
-
});
|
|
27
|
-
assert.deepEqual(target, {
|
|
28
|
-
threadId: "t1",
|
|
29
|
-
mailboxId: "mb1",
|
|
30
|
-
subject: "Loaded subject",
|
|
31
|
-
messageId: "m1",
|
|
32
|
-
authenticity,
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// The bug: a tapped semantic "Related" hit's message isn't in the loaded list,
|
|
37
|
-
// so there is no thread to resolve — it must still open from the URL hints.
|
|
38
|
-
test("falls back to the URL thread + mailbox when no thread is loaded", () => {
|
|
39
|
-
const target = buildConversationTarget(undefined, {
|
|
40
|
-
messageId: "m9",
|
|
41
|
-
threadId: "t9",
|
|
42
|
-
mailboxId: "mb9",
|
|
43
|
-
});
|
|
44
|
-
assert.deepEqual(target, {
|
|
45
|
-
threadId: "t9",
|
|
46
|
-
mailboxId: "mb9",
|
|
47
|
-
messageId: "m9",
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("returns undefined when the message is cleared (Back pressed)", () => {
|
|
52
|
-
const target = buildConversationTarget(undefined, {
|
|
53
|
-
messageId: undefined,
|
|
54
|
-
threadId: "t9",
|
|
55
|
-
mailboxId: "mb9",
|
|
56
|
-
});
|
|
57
|
-
assert.equal(target, undefined);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("returns undefined without a threadId to open", () => {
|
|
61
|
-
const target = buildConversationTarget(undefined, {
|
|
62
|
-
messageId: "m9",
|
|
63
|
-
threadId: undefined,
|
|
64
|
-
mailboxId: "mb9",
|
|
65
|
-
});
|
|
66
|
-
assert.equal(target, undefined);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
// Regression test for #623: search results must be openable once the query
|
|
2
|
-
// settles. The route computes `isSearchPending = searchInput !== searchQuery`
|
|
3
|
-
// to decide whether to suppress the reading pane, then resolves the selected
|
|
4
|
-
// thread via `resolveSelectedThread`.
|
|
5
|
-
//
|
|
6
|
-
// - `searchInput` is the live (pre-debounce) value the user is typing.
|
|
7
|
-
// - `searchQuery` is the debounced (committed) value sent to the API.
|
|
8
|
-
// - The pane is suppressed only while the two differ (a debounce is in flight).
|
|
9
|
-
// - Once settled, a selected result should be shown in the reading pane.
|
|
10
|
-
|
|
11
|
-
import assert from "node:assert";
|
|
12
|
-
import { describe, test } from "node:test";
|
|
13
|
-
import {
|
|
14
|
-
isSearchPending,
|
|
15
|
-
resolveOpenThread,
|
|
16
|
-
resolveSelectedThread,
|
|
17
|
-
} from "./search-pending.ts";
|
|
18
|
-
|
|
19
|
-
describe("isSearchPending (reading-pane suppression guard)", () => {
|
|
20
|
-
test("no search: both empty — not pending", () => {
|
|
21
|
-
assert.equal(isSearchPending("", ""), false);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("settled search: input matches debounced query — not pending", () => {
|
|
25
|
-
assert.equal(isSearchPending("amazon", "amazon"), false);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("user starts typing: input differs from query — pending", () => {
|
|
29
|
-
assert.equal(isSearchPending("ama", ""), true);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("mid-debounce: partial input vs previous query — pending", () => {
|
|
33
|
-
assert.equal(isSearchPending("amaz", "amazon"), true);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe("resolveSelectedThread (#623 reading-pane resolution)", () => {
|
|
38
|
-
const threads = [{ messageId: "msg-001" }, { messageId: "msg-002" }];
|
|
39
|
-
|
|
40
|
-
test("regression #623: settled query with a matching selection opens the thread", () => {
|
|
41
|
-
const pending = isSearchPending("amazon", "amazon");
|
|
42
|
-
assert.equal(pending, false);
|
|
43
|
-
|
|
44
|
-
const selectedThread = resolveSelectedThread(threads, "msg-001", pending);
|
|
45
|
-
assert.deepEqual(selectedThread, { messageId: "msg-001" });
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("pending search suppresses the reading pane even with a matching selection", () => {
|
|
49
|
-
const pending = isSearchPending("amaz", "amazon");
|
|
50
|
-
assert.equal(pending, true);
|
|
51
|
-
|
|
52
|
-
const selectedThread = resolveSelectedThread(threads, "msg-001", pending);
|
|
53
|
-
assert.equal(selectedThread, undefined);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("settled query but selection absent from results — no thread", () => {
|
|
57
|
-
const selectedThread = resolveSelectedThread(threads, "msg-999", false);
|
|
58
|
-
assert.equal(selectedThread, undefined);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("settled query with no selection — no thread", () => {
|
|
62
|
-
const selectedThread = resolveSelectedThread(threads, undefined, false);
|
|
63
|
-
assert.equal(selectedThread, undefined);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("resolveOpenThread (#306 filtered list)", () => {
|
|
68
|
-
const listed = { messageId: "msg-001", subject: "listed" };
|
|
69
|
-
const snapshot = { messageId: "msg-001", subject: "snapshot" };
|
|
70
|
-
|
|
71
|
-
test("prefers the row the list returned", () => {
|
|
72
|
-
assert.equal(resolveOpenThread(listed, snapshot, "msg-001", false), listed);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("keeps the open thread when a filter pages it out of the list", () => {
|
|
76
|
-
assert.equal(
|
|
77
|
-
resolveOpenThread(undefined, snapshot, "msg-001", false),
|
|
78
|
-
snapshot,
|
|
79
|
-
);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("drops a snapshot of a message that is no longer selected", () => {
|
|
83
|
-
assert.equal(
|
|
84
|
-
resolveOpenThread(undefined, snapshot, "msg-002", false),
|
|
85
|
-
undefined,
|
|
86
|
-
);
|
|
87
|
-
assert.equal(
|
|
88
|
-
resolveOpenThread(undefined, snapshot, undefined, false),
|
|
89
|
-
undefined,
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("a pending search still closes the pane (#539)", () => {
|
|
94
|
-
assert.equal(
|
|
95
|
-
resolveOpenThread(undefined, snapshot, "msg-001", true),
|
|
96
|
-
undefined,
|
|
97
|
-
);
|
|
98
|
-
});
|
|
99
|
-
});
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// Reading-pane suppression guard for the mail search bar (#539, #623).
|
|
2
|
-
//
|
|
3
|
-
// `searchInput` is the live (pre-debounce) value the user is typing;
|
|
4
|
-
// `searchQuery` is the debounced (committed) value sent to the API. A debounce
|
|
5
|
-
// is in flight whenever the two differ — during that window the reading pane is
|
|
6
|
-
// kept closed so it clears the instant a new search starts (#539). Once the
|
|
7
|
-
// query settles, a selected result is honored so search results can be opened
|
|
8
|
-
// (#623).
|
|
9
|
-
export const isSearchPending = (
|
|
10
|
-
searchInput: string,
|
|
11
|
-
searchQuery: string,
|
|
12
|
-
): boolean => searchInput !== searchQuery;
|
|
13
|
-
|
|
14
|
-
// Resolve the thread to show in the reading pane. Returns `undefined` while a
|
|
15
|
-
// search debounce is pending or when nothing is selected; otherwise looks the
|
|
16
|
-
// selected message up in the loaded thread list.
|
|
17
|
-
export const resolveSelectedThread = <T extends { messageId: string }>(
|
|
18
|
-
threads: T[],
|
|
19
|
-
selectedMessageId: string | undefined,
|
|
20
|
-
pending: boolean,
|
|
21
|
-
): T | undefined => {
|
|
22
|
-
if (pending || !selectedMessageId) return undefined;
|
|
23
|
-
return threads.find((t) => t.messageId === selectedMessageId);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* The thread the reading pane shows, once the list is a server-side query
|
|
28
|
-
* (#306).
|
|
29
|
-
*
|
|
30
|
-
* The list used to hold every loaded row and filter a copy, so an open thread
|
|
31
|
-
* could always be found again in the unfiltered set. A filtered list is now the
|
|
32
|
-
* server's answer to one predicate, and a chip the open message does not match
|
|
33
|
-
* pages it out — so what the user opened is kept as a snapshot and answers for
|
|
34
|
-
* itself until they open something else. That is a derivation over the user's
|
|
35
|
-
* own selection, which stays on this side of the boundary.
|
|
36
|
-
*
|
|
37
|
-
* The snapshot is ignored while a search debounce is pending, so the pane still
|
|
38
|
-
* clears the instant a new search starts (#539).
|
|
39
|
-
*/
|
|
40
|
-
export const resolveOpenThread = <T extends { messageId: string }>(
|
|
41
|
-
listed: T | undefined,
|
|
42
|
-
opened: T | undefined,
|
|
43
|
-
selectedMessageId: string | undefined,
|
|
44
|
-
pending: boolean,
|
|
45
|
-
): T | undefined => {
|
|
46
|
-
if (listed) return listed;
|
|
47
|
-
if (pending || !selectedMessageId) return undefined;
|
|
48
|
-
return opened?.messageId === selectedMessageId ? opened : undefined;
|
|
49
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { useParams } from "@tanstack/react-router";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
|
-
|
|
4
|
-
/** The conversation the brief has open, as the address states it. */
|
|
5
|
-
export interface BriefThreadPath {
|
|
6
|
-
threadId: string;
|
|
7
|
-
/**
|
|
8
|
-
* Which message inside the thread is expanded and scrolled to. Absent on a
|
|
9
|
-
* bare thread address, where the newest message answers for the conversation.
|
|
10
|
-
*/
|
|
11
|
-
messageId: string | undefined;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* A conversation to open. The thread is what the pane fetches by; the message is
|
|
16
|
-
* the row the reader pointed at, so a list always knows both.
|
|
17
|
-
*/
|
|
18
|
-
export interface BriefThreadTarget {
|
|
19
|
-
threadId: string;
|
|
20
|
-
messageId: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* The thread the brief has open, read off the path.
|
|
25
|
-
*
|
|
26
|
-
* The thread is a child route of the list, and the list layout mounts the pane
|
|
27
|
-
* above the `Outlet` — so it asks the router which of its children matched
|
|
28
|
-
* rather than reading a param it does not own. Each `from` names a real route,
|
|
29
|
-
* so a segment that does not exist fails to compile.
|
|
30
|
-
*/
|
|
31
|
-
export function useBriefThreadPath(): BriefThreadPath | undefined {
|
|
32
|
-
const thread = useParams({
|
|
33
|
-
from: "/mail/brief/$threadId",
|
|
34
|
-
shouldThrow: false,
|
|
35
|
-
});
|
|
36
|
-
const message = useParams({
|
|
37
|
-
from: "/mail/brief/$threadId/$messageId",
|
|
38
|
-
shouldThrow: false,
|
|
39
|
-
});
|
|
40
|
-
const threadId = thread?.threadId;
|
|
41
|
-
const messageId = message?.messageId;
|
|
42
|
-
|
|
43
|
-
return useMemo(
|
|
44
|
-
() => (threadId ? { threadId, messageId } : undefined),
|
|
45
|
-
[threadId, messageId],
|
|
46
|
-
);
|
|
47
|
-
}
|