@remit/web-client 0.0.107 → 0.0.108
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.
|
|
3
|
+
"version": "0.0.108",
|
|
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": {
|
|
@@ -42,8 +42,10 @@ import {
|
|
|
42
42
|
type FilterSheetProps,
|
|
43
43
|
type FilterSheetSource,
|
|
44
44
|
KeyboardHintBar,
|
|
45
|
+
partitionSpamResults,
|
|
45
46
|
type SearchResult,
|
|
46
47
|
SelectionTopBar,
|
|
48
|
+
SpamResultsOffer,
|
|
47
49
|
type ThreadRowData,
|
|
48
50
|
type ThreadSection,
|
|
49
51
|
} from "@remit/ui";
|
|
@@ -81,6 +83,7 @@ import type { ListHeaderChrome } from "@/lib/list-header-chrome";
|
|
|
81
83
|
import { useMailContext } from "@/lib/mail-context";
|
|
82
84
|
import { relatedSearchResults, rowToSearchResult } from "@/lib/search-result";
|
|
83
85
|
import { parseSearchTokens } from "@/lib/search-tokens";
|
|
86
|
+
import { spamOfferForResults } from "@/lib/spam-offer";
|
|
84
87
|
import {
|
|
85
88
|
type SelectionWizardControl,
|
|
86
89
|
useSelectionWizard,
|
|
@@ -463,6 +466,7 @@ export function DailyBrief({
|
|
|
463
466
|
const tokenContext = useSearchTokenContext();
|
|
464
467
|
const isDesktop = useIsDesktop();
|
|
465
468
|
const wizard = useSelectionWizard();
|
|
469
|
+
const navigate = useNavigate();
|
|
466
470
|
|
|
467
471
|
const nonMuted = useMemo(
|
|
468
472
|
() => sortAccountsByCreatedAt(accounts.filter((a) => !a.muted?.value)),
|
|
@@ -592,9 +596,33 @@ export function DailyBrief({
|
|
|
592
596
|
);
|
|
593
597
|
}, [threadsData, searchData, selectedAccountId, sq, queryTokens]);
|
|
594
598
|
|
|
599
|
+
// The unscoped search reaches every folder, Spam included (see `filteredRows`
|
|
600
|
+
// above), and the brief is the one global-scope view whose own rows now stand
|
|
601
|
+
// in for the read-only results panel once the query commits. The panel held
|
|
602
|
+
// Spam out and offered a way to it instead (`MailListHeader`'s `spamOffer`);
|
|
603
|
+
// the brief's own body has to do the same, or a committed search surfaces
|
|
604
|
+
// junk mail inline and drops the way back to it. A bare token query (e.g.
|
|
605
|
+
// `is:unread`) never reaches the widened endpoint, so there is nothing to
|
|
606
|
+
// hold out.
|
|
607
|
+
const { bodyRows, briefSpamOffer } = useMemo(() => {
|
|
608
|
+
if (!sq) return { bodyRows: filteredRows, briefSpamOffer: undefined };
|
|
609
|
+
const asResults = filteredRows.map((row) =>
|
|
610
|
+
rowToSearchResult(row, resultFolderIndex),
|
|
611
|
+
);
|
|
612
|
+
const { spam } = partitionSpamResults(asResults);
|
|
613
|
+
if (spam.length === 0) {
|
|
614
|
+
return { bodyRows: filteredRows, briefSpamOffer: undefined };
|
|
615
|
+
}
|
|
616
|
+
const spamIds = new Set(spam.map((result) => result.id));
|
|
617
|
+
return {
|
|
618
|
+
bodyRows: filteredRows.filter((row) => !spamIds.has(row.id)),
|
|
619
|
+
briefSpamOffer: spamOfferForResults(asResults),
|
|
620
|
+
};
|
|
621
|
+
}, [filteredRows, sq, resultFolderIndex]);
|
|
622
|
+
|
|
595
623
|
const sections = useMemo<ThreadSection[]>(
|
|
596
|
-
() => groupBriefSections(
|
|
597
|
-
[
|
|
624
|
+
() => groupBriefSections(bodyRows),
|
|
625
|
+
[bodyRows],
|
|
598
626
|
);
|
|
599
627
|
|
|
600
628
|
const accountSources = useMemo<FilterSheetSource[]>(() => {
|
|
@@ -693,10 +721,14 @@ export function DailyBrief({
|
|
|
693
721
|
]);
|
|
694
722
|
|
|
695
723
|
// The brief is genuinely empty (caught up) only when nothing is narrowing the
|
|
696
|
-
// view: no account source and no search. When a
|
|
697
|
-
// the list says so instead, so the narrowing
|
|
724
|
+
// view: no account source and no search — free text or token. When a
|
|
725
|
+
// source/search yields nothing, the list says so instead, so the narrowing
|
|
726
|
+
// is still visible as the reason.
|
|
698
727
|
const caughtUp =
|
|
699
|
-
sections.length === 0 &&
|
|
728
|
+
sections.length === 0 &&
|
|
729
|
+
selectedAccountId === "all" &&
|
|
730
|
+
sq.length === 0 &&
|
|
731
|
+
queryTokens.length === 0;
|
|
700
732
|
|
|
701
733
|
// "Caught up" is a claim about the user's mail, so it may only be made once
|
|
702
734
|
// the server has confirmed no sync is running (#452). The config snapshot
|
|
@@ -752,12 +784,32 @@ export function DailyBrief({
|
|
|
752
784
|
briefSkeleton
|
|
753
785
|
)
|
|
754
786
|
) : (
|
|
755
|
-
<
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
787
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
788
|
+
{briefSpamOffer && (
|
|
789
|
+
<SpamResultsOffer
|
|
790
|
+
count={briefSpamOffer.count}
|
|
791
|
+
onScopeToSpam={() =>
|
|
792
|
+
navigate({
|
|
793
|
+
to: "/mail/$mailboxId",
|
|
794
|
+
params: { mailboxId: briefSpamOffer.mailboxId },
|
|
795
|
+
search: {
|
|
796
|
+
q: searchQuery || undefined,
|
|
797
|
+
selectedMessageId: undefined,
|
|
798
|
+
selectedThreadId: undefined,
|
|
799
|
+
},
|
|
800
|
+
})
|
|
801
|
+
}
|
|
802
|
+
/>
|
|
803
|
+
)}
|
|
804
|
+
<div className="min-h-0 flex-1">
|
|
805
|
+
<BriefListBody
|
|
806
|
+
sections={sections}
|
|
807
|
+
briefCategory={selectedCategory}
|
|
808
|
+
selectedThreadId={selectedMessageId}
|
|
809
|
+
onSelectThread={onSelectMessage}
|
|
810
|
+
/>
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
761
813
|
);
|
|
762
814
|
|
|
763
815
|
// The cursor and selection wrap the whole pane, not just the list body: the
|
|
@@ -785,6 +837,12 @@ export function DailyBrief({
|
|
|
785
837
|
relatedResults,
|
|
786
838
|
relatedLoading,
|
|
787
839
|
onSelectSearchResult,
|
|
840
|
+
// The body already narrows to the committed query
|
|
841
|
+
// (`matchesBriefSearch` + `matchesSearchTokens` + the server `query`,
|
|
842
|
+
// above), so a committed search is a selectable list here exactly as
|
|
843
|
+
// it is on the mailbox route (#212) — the two-engine panel stays for
|
|
844
|
+
// the typing/uncommitted state only.
|
|
845
|
+
searchResultsInBody: true,
|
|
788
846
|
}}
|
|
789
847
|
rows={filteredRows}
|
|
790
848
|
>
|
|
@@ -237,6 +237,11 @@ export function FlaggedList({
|
|
|
237
237
|
searchResults={searchResults}
|
|
238
238
|
searchLoading={isLoading}
|
|
239
239
|
onSelectSearchResult={(result) => onSelectMessage?.(result.id)}
|
|
240
|
+
// A committed search renders in this view's own rows (`rows` already
|
|
241
|
+
// narrows to the query above), so the multi-select toolbar stays
|
|
242
|
+
// reachable exactly as it does on the mailbox route (#212). The
|
|
243
|
+
// two-engine panel stays for the typing/uncommitted state only.
|
|
244
|
+
searchResultsInBody
|
|
240
245
|
>
|
|
241
246
|
<ThreadListInteraction
|
|
242
247
|
selectedMessageId={selectedMessageId}
|
|
@@ -129,13 +129,13 @@ export interface MailListHeaderProps {
|
|
|
129
129
|
relatedResultsLabel?: string;
|
|
130
130
|
/**
|
|
131
131
|
* The body renders the committed search itself as a selectable list — the
|
|
132
|
-
* mailbox route's `MessageList`
|
|
133
|
-
* multi-select toolbar and the
|
|
134
|
-
*
|
|
135
|
-
* swapping in the read-only
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* and
|
|
132
|
+
* mailbox route's `MessageList` and the brief and Starred's own rows filter
|
|
133
|
+
* to the search and host the multi-select toolbar and (on the mailbox route)
|
|
134
|
+
* the "Select all N matching" escalation (#212). When set, a committed query
|
|
135
|
+
* keeps the body on tablet/desktop instead of swapping in the read-only
|
|
136
|
+
* two-engine `SearchResults` panel; the panel still shows while the query is
|
|
137
|
+
* being typed (uncommitted). A view whose body is not a search-result list
|
|
138
|
+
* leaves this unset and keeps the panel for every query.
|
|
139
139
|
*/
|
|
140
140
|
searchResultsInBody?: boolean;
|
|
141
141
|
}
|
|
@@ -336,12 +336,12 @@ export function MailListHeader({
|
|
|
336
336
|
// It belongs to the search, not to any one way of showing it. The affordance
|
|
337
337
|
// therefore sits in the pane, above whichever body is up — the read-only
|
|
338
338
|
// results panel, or a list whose own rows narrow to the committed query — and
|
|
339
|
-
// is mounted on the same condition as the query itself. Rendering it inside
|
|
340
|
-
// results panel made it a mailbox's flash: the panel shows while the query
|
|
341
|
-
// being typed and hands back to `MessageList` the moment the query commits
|
|
342
|
-
// the URL, taking the affordance down with it a few hundred milliseconds
|
|
343
|
-
// it appeared
|
|
344
|
-
//
|
|
339
|
+
// is mounted on the same condition as the query itself. Rendering it inside
|
|
340
|
+
// the results panel made it a mailbox's flash: the panel shows while the query
|
|
341
|
+
// is being typed and hands back to `MessageList` the moment the query commits
|
|
342
|
+
// to the URL, taking the affordance down with it a few hundred milliseconds
|
|
343
|
+
// after it appeared. Mounting it here instead keeps it up through that
|
|
344
|
+
// hand-back on every view.
|
|
345
345
|
const searchHadSemanticReach = related.length > 0;
|
|
346
346
|
const conversion = useMemo(
|
|
347
347
|
() => convertSearchToRule(parsed, { searchHadSemanticReach }),
|
|
@@ -372,10 +372,11 @@ export function MailListHeader({
|
|
|
372
372
|
// Tablet + desktop keep the inline toolbar search; while a query is being
|
|
373
373
|
// typed the list-pane body swaps to the same sectioned results the phone
|
|
374
374
|
// takeover shows, under the same FilterSheet. A view whose own body renders
|
|
375
|
-
// the committed search as a selectable list (`searchResultsInBody
|
|
376
|
-
// mailbox route) keeps the panel only until the query
|
|
377
|
-
// then hands back to its
|
|
378
|
-
//
|
|
375
|
+
// the committed search as a selectable list (`searchResultsInBody` — the
|
|
376
|
+
// mailbox route, the brief, Starred) keeps the panel only until the query
|
|
377
|
+
// commits to the URL, then hands back to its own rows so multi-select (and,
|
|
378
|
+
// on the mailbox route, the "Select all N matching" escalation) is reachable
|
|
379
|
+
// (#212). Clearing the query restores the normal list.
|
|
379
380
|
const showInlineResults = showInlineSearchResults({
|
|
380
381
|
tier,
|
|
381
382
|
hasLiveInput: hasQuery,
|
|
@@ -54,7 +54,8 @@ interface MailViewChromeProps {
|
|
|
54
54
|
/**
|
|
55
55
|
* The body renders the committed search as a selectable list itself; see
|
|
56
56
|
* `MailListHeader`. The mailbox route sets this so a committed search yields
|
|
57
|
-
* its multi-select `MessageList` (#212);
|
|
57
|
+
* its multi-select `MessageList` (#212); Starred sets it for the same reason,
|
|
58
|
+
* over its own already query-narrowed rows.
|
|
58
59
|
*/
|
|
59
60
|
searchResultsInBody?: boolean;
|
|
60
61
|
}
|
|
@@ -13,12 +13,12 @@ import type { LayoutTier } from "@/hooks/useLayoutTier";
|
|
|
13
13
|
* Phone never swaps: it shows the body and keeps the panel in its full-screen
|
|
14
14
|
* takeover instead, so this only decides tablet/desktop.
|
|
15
15
|
*
|
|
16
|
-
* A view whose body renders committed results itself (`bodyRendersCommittedResults
|
|
17
|
-
* the mailbox route) keeps the panel only while the query is
|
|
18
|
-
* uncommitted, not yet mirrored to the URL — and hands back to
|
|
19
|
-
* the query commits, so the committed search is a selectable
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* A view whose body renders committed results itself (`bodyRendersCommittedResults`
|
|
17
|
+
* — the mailbox route, the brief, Starred) keeps the panel only while the query is
|
|
18
|
+
* still being typed — uncommitted, not yet mirrored to the URL — and hands back to
|
|
19
|
+
* the body the moment the query commits, so the committed search is a selectable
|
|
20
|
+
* list. A view that leaves the flag off keeps the panel for any query, committed
|
|
21
|
+
* or not.
|
|
22
22
|
*/
|
|
23
23
|
export const showInlineSearchResults = (input: {
|
|
24
24
|
tier: LayoutTier;
|