@remit/web-client 0.0.80 → 0.0.82
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/mail/DailyBrief.tsx +5 -2
- package/src/components/settings/FilterEditor.render.test.ts +1 -0
- package/src/components/settings/FiltersList.render.test.ts +1 -0
- package/src/components/settings/settings-filter.stories.tsx +1 -0
- package/src/hooks/useFilters.test.ts +1 -0
- package/src/hooks/useToggleStar.test.ts +1 -0
- package/src/lib/brief.test.ts +77 -0
- package/src/lib/brief.ts +18 -1
- package/src/lib/dedupe-thread-messages.test.ts +1 -0
- package/src/lib/organize/filter-edit-model.test.ts +1 -0
- package/src/test-support/fixtures.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.82",
|
|
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": {
|
|
@@ -41,6 +41,7 @@ import { useSemanticSearch } from "@/hooks/useSemanticSearch";
|
|
|
41
41
|
import type { TriageContextUpdate } from "@/hooks/useTriageLayer";
|
|
42
42
|
import { sortAccountsByCreatedAt } from "@/lib/account-order";
|
|
43
43
|
import {
|
|
44
|
+
excludeMutedSenders,
|
|
44
45
|
groupBriefSections,
|
|
45
46
|
matchesBriefSearch,
|
|
46
47
|
matchesSearchTokens,
|
|
@@ -275,12 +276,14 @@ export function DailyBrief({
|
|
|
275
276
|
// `BriefSections` filter row's job, so the full per-category sections are
|
|
276
277
|
// handed to it; it groups, narrows, and flattens.
|
|
277
278
|
const filteredRows = useMemo<ThreadRowData[]>(() => {
|
|
278
|
-
const briefRows = (threadsData?.items ?? []).map(
|
|
279
|
+
const briefRows = excludeMutedSenders(threadsData?.items ?? []).map(
|
|
280
|
+
toThreadRowData,
|
|
281
|
+
);
|
|
279
282
|
// No free text: the brief list as it comes, order untouched.
|
|
280
283
|
const rows = sq
|
|
281
284
|
? mergeSearchRows(
|
|
282
285
|
briefRows.filter((t) => matchesBriefSearch(t, sq)),
|
|
283
|
-
(searchData?.items ?? []).map(toThreadRowData),
|
|
286
|
+
excludeMutedSenders(searchData?.items ?? []).map(toThreadRowData),
|
|
284
287
|
)
|
|
285
288
|
: briefRows;
|
|
286
289
|
return rows.filter(
|
package/src/lib/brief.test.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { describe, test } from "node:test";
|
|
|
3
3
|
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
4
4
|
import type { ThreadRowData } from "@remit/ui";
|
|
5
5
|
import {
|
|
6
|
+
excludeMutedSenders,
|
|
6
7
|
groupBriefSections,
|
|
7
8
|
matchesBriefSearch,
|
|
8
9
|
matchesSearchTokens,
|
|
@@ -32,6 +33,7 @@ function threadResponse(
|
|
|
32
33
|
hasStars: false,
|
|
33
34
|
star: "none",
|
|
34
35
|
senderTrust: "unknown",
|
|
36
|
+
muted: false,
|
|
35
37
|
createdAt: 0,
|
|
36
38
|
updatedAt: 0,
|
|
37
39
|
...overrides,
|
|
@@ -265,6 +267,81 @@ describe("groupBriefSections", () => {
|
|
|
265
267
|
});
|
|
266
268
|
});
|
|
267
269
|
|
|
270
|
+
// issue #301: `Address.flags.muted` is denormalized onto each row as
|
|
271
|
+
// `muted`; the brief excludes those rows before grouping into sections.
|
|
272
|
+
describe("excludeMutedSenders", () => {
|
|
273
|
+
test("drops a row whose sender is muted", () => {
|
|
274
|
+
const kept = threadResponse({ threadMessageId: "keep" });
|
|
275
|
+
const muted = threadResponse({ threadMessageId: "mute-me", muted: true });
|
|
276
|
+
const result = excludeMutedSenders([kept, muted]);
|
|
277
|
+
assert.deepStrictEqual(
|
|
278
|
+
result.map((t) => t.threadMessageId),
|
|
279
|
+
["keep"],
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("keeps rows whose sender is not muted", () => {
|
|
284
|
+
const rows = [
|
|
285
|
+
threadResponse({ threadMessageId: "a", muted: false }),
|
|
286
|
+
threadResponse({ threadMessageId: "b" }),
|
|
287
|
+
];
|
|
288
|
+
assert.strictEqual(excludeMutedSenders(rows).length, 2);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("returns an empty array when every sender is muted", () => {
|
|
292
|
+
const rows = [
|
|
293
|
+
threadResponse({ threadMessageId: "a", muted: true }),
|
|
294
|
+
threadResponse({ threadMessageId: "b", muted: true }),
|
|
295
|
+
];
|
|
296
|
+
assert.deepStrictEqual(excludeMutedSenders(rows), []);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("a muted sender's message is excluded from every section, not folded into uncategorized", () => {
|
|
300
|
+
const rows = [
|
|
301
|
+
threadResponse({
|
|
302
|
+
threadMessageId: "muted-personal",
|
|
303
|
+
messageId: "muted-personal",
|
|
304
|
+
category: "personal",
|
|
305
|
+
muted: true,
|
|
306
|
+
}),
|
|
307
|
+
threadResponse({
|
|
308
|
+
threadMessageId: "kept-personal",
|
|
309
|
+
messageId: "kept-personal",
|
|
310
|
+
category: "personal",
|
|
311
|
+
}),
|
|
312
|
+
];
|
|
313
|
+
const sections = groupBriefSections(
|
|
314
|
+
excludeMutedSenders(rows).map(toThreadRowData),
|
|
315
|
+
);
|
|
316
|
+
const allIds = sections.flatMap((s) => s.threads.map((t) => t.id));
|
|
317
|
+
assert.deepStrictEqual(allIds, ["kept-personal"]);
|
|
318
|
+
for (const section of sections) {
|
|
319
|
+
assert.ok(!section.threads.some((t) => t.id === "muted-personal"));
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("muting every candidate row leaves no sections (brief empty state)", () => {
|
|
324
|
+
const rows = [
|
|
325
|
+
threadResponse({
|
|
326
|
+
threadMessageId: "a",
|
|
327
|
+
messageId: "a",
|
|
328
|
+
category: "personal",
|
|
329
|
+
muted: true,
|
|
330
|
+
}),
|
|
331
|
+
threadResponse({
|
|
332
|
+
threadMessageId: "b",
|
|
333
|
+
messageId: "b",
|
|
334
|
+
category: "newsletter",
|
|
335
|
+
muted: true,
|
|
336
|
+
}),
|
|
337
|
+
];
|
|
338
|
+
const sections = groupBriefSections(
|
|
339
|
+
excludeMutedSenders(rows).map(toThreadRowData),
|
|
340
|
+
);
|
|
341
|
+
assert.deepStrictEqual(sections, []);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
268
345
|
describe("matchesBriefSearch", () => {
|
|
269
346
|
const r = row({
|
|
270
347
|
id: "1",
|
package/src/lib/brief.ts
CHANGED
|
@@ -24,7 +24,11 @@
|
|
|
24
24
|
* a high-volume mailbox read≠handled and unread≠important; unread is a
|
|
25
25
|
* user-selectable filter chip instead.
|
|
26
26
|
*
|
|
27
|
-
* Muted senders
|
|
27
|
+
* Muted senders and empty sections are excluded. Mute filtering happens in
|
|
28
|
+
* `excludeMutedSenders`, applied by the caller to the raw thread rows before
|
|
29
|
+
* `toThreadRowData`/grouping — the server denormalizes `muted` onto each row
|
|
30
|
+
* from the From address's flags (RFC 039 Decision 3, issue #301), so no
|
|
31
|
+
* client-side Address lookup is needed.
|
|
28
32
|
*/
|
|
29
33
|
|
|
30
34
|
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
@@ -67,6 +71,19 @@ export function toThreadRowData(
|
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Excludes rows whose From address is muted (`thread.muted === true`,
|
|
76
|
+
* denormalized server-side from `Address.flags.muted`). Muting hides a
|
|
77
|
+
* sender from the brief only — it never deletes, marks read, or moves their
|
|
78
|
+
* mail, so callers outside the brief (mailbox listings, search) must not
|
|
79
|
+
* apply this filter.
|
|
80
|
+
*/
|
|
81
|
+
export function excludeMutedSenders(
|
|
82
|
+
threads: RemitImapThreadMessageResponse[],
|
|
83
|
+
): RemitImapThreadMessageResponse[] {
|
|
84
|
+
return threads.filter((t) => t.muted !== true);
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
/**
|
|
71
88
|
* Union of the brief's own rows with the rows the server's cross-folder search
|
|
72
89
|
* returned, newest first.
|