@remit/ui 0.0.112 → 0.0.113
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/app-shell-types.ts +10 -0
- package/src/components/intelligence-panel.render.test.ts +94 -0
- package/src/components/intelligence-panel.stories.tsx +5 -5
- package/src/components/intelligence-panel.tsx +3 -5
- package/src/components/mobile-reading-pane.render.test.ts +1 -1
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -435,3 +435,13 @@ export const categoryTone: Record<
|
|
|
435
435
|
transactional: "positive",
|
|
436
436
|
social: "warning",
|
|
437
437
|
};
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Whether a string names one of the classifier's categories. A host reading a
|
|
441
|
+
* category off the API narrows it with this rather than asserting it: a value
|
|
442
|
+
* from a newer server that this build has no tone for is a value it cannot
|
|
443
|
+
* render, and it needs to know that rather than find out at lookup time.
|
|
444
|
+
*/
|
|
445
|
+
export function isThreadCategory(value: string): value is ThreadCategory {
|
|
446
|
+
return Object.hasOwn(categoryTone, value);
|
|
447
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { createElement } from "react";
|
|
4
|
+
import { renderToString } from "react-dom/server";
|
|
5
|
+
import { categoryTone, isThreadCategory } from "./app-shell-types.js";
|
|
6
|
+
import type { IntelligenceData } from "./intelligence-panel.js";
|
|
7
|
+
import { IntelligencePanel } from "./intelligence-panel.js";
|
|
8
|
+
|
|
9
|
+
const vipWithFailingAuthenticity: IntelligenceData = {
|
|
10
|
+
sender: {
|
|
11
|
+
name: "Sabrina Basten",
|
|
12
|
+
email: "sabrina@sabrinabasten.example",
|
|
13
|
+
trust: "vip",
|
|
14
|
+
firstSeenLabel: "Mar 2023",
|
|
15
|
+
inboundCount: 214,
|
|
16
|
+
replyCount: 188,
|
|
17
|
+
},
|
|
18
|
+
authenticity: {
|
|
19
|
+
verdict: "mismatch",
|
|
20
|
+
fromDomain: "sabrinabasten.example",
|
|
21
|
+
dkimDomain: "custmx.one.example",
|
|
22
|
+
summary: "Signed by custmx.one.example instead.",
|
|
23
|
+
},
|
|
24
|
+
category: { value: "personal" },
|
|
25
|
+
flags: { vip: true },
|
|
26
|
+
similar: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** The rendered chip in the Category section. */
|
|
30
|
+
function categoryChip(data: IntelligenceData): string {
|
|
31
|
+
const html = renderToString(createElement(IntelligencePanel, { data }));
|
|
32
|
+
const marker = `>${data.category.value}</span>`;
|
|
33
|
+
const end = html.indexOf(marker);
|
|
34
|
+
assert.notEqual(end, -1, `expected a chip reading '${data.category.value}'`);
|
|
35
|
+
const start = html.lastIndexOf("<span", end);
|
|
36
|
+
return html.slice(start, end + marker.length);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("IntelligencePanel category chip", () => {
|
|
40
|
+
it("takes its colour from the category, not the authenticity verdict", () => {
|
|
41
|
+
const chip = categoryChip(vipWithFailingAuthenticity);
|
|
42
|
+
assert.doesNotMatch(chip, /danger/);
|
|
43
|
+
assert.match(chip, /text-accent-2/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders the same chip for a category whatever the verdict said", () => {
|
|
47
|
+
const failing = categoryChip(vipWithFailingAuthenticity);
|
|
48
|
+
const aligned = categoryChip({
|
|
49
|
+
...vipWithFailingAuthenticity,
|
|
50
|
+
authenticity: {
|
|
51
|
+
verdict: "aligned",
|
|
52
|
+
fromDomain: "sabrinabasten.example",
|
|
53
|
+
summary: "Nothing looks unusual about this sender.",
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
assert.equal(failing, aligned);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("gives each category its own tone", () => {
|
|
60
|
+
const receipt = categoryChip({
|
|
61
|
+
...vipWithFailingAuthenticity,
|
|
62
|
+
category: { value: "transactional" },
|
|
63
|
+
});
|
|
64
|
+
assert.match(receipt, /text-positive/);
|
|
65
|
+
const newsletter = categoryChip({
|
|
66
|
+
...vipWithFailingAuthenticity,
|
|
67
|
+
category: { value: "newsletter" },
|
|
68
|
+
});
|
|
69
|
+
assert.match(newsletter, /text-fg-muted/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("renders a readable chip for a message the classifier placed nowhere", () => {
|
|
73
|
+
const chip = categoryChip({
|
|
74
|
+
...vipWithFailingAuthenticity,
|
|
75
|
+
category: { value: "uncategorized" },
|
|
76
|
+
});
|
|
77
|
+
assert.match(chip, /text-fg-muted/);
|
|
78
|
+
assert.doesNotMatch(chip, /danger/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("isThreadCategory", () => {
|
|
83
|
+
it("accepts every category the tone map covers", () => {
|
|
84
|
+
for (const category of Object.keys(categoryTone)) {
|
|
85
|
+
assert.equal(isThreadCategory(category), true, category);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("rejects a category this build has no tone for", () => {
|
|
90
|
+
assert.equal(isThreadCategory("invoice"), false);
|
|
91
|
+
assert.equal(isThreadCategory("Personal"), false);
|
|
92
|
+
assert.equal(isThreadCategory("toString"), false);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -15,7 +15,7 @@ const base: IntelligenceData = {
|
|
|
15
15
|
dkimDomain: "example.com",
|
|
16
16
|
summary: "This message was signed by example.com.",
|
|
17
17
|
},
|
|
18
|
-
category: { value: "
|
|
18
|
+
category: { value: "personal" },
|
|
19
19
|
similar: [],
|
|
20
20
|
};
|
|
21
21
|
|
|
@@ -62,7 +62,7 @@ export const SignedButUnrecognised: Story = {
|
|
|
62
62
|
trust: "unknown",
|
|
63
63
|
firstSeenLabel: "today",
|
|
64
64
|
},
|
|
65
|
-
category: { value: "
|
|
65
|
+
category: { value: "automated" },
|
|
66
66
|
authenticity: {
|
|
67
67
|
verdict: "caution",
|
|
68
68
|
fromDomain: "serviceupdatebank.atlassian.net",
|
|
@@ -88,7 +88,7 @@ export const SignedButLookalikeName: Story = {
|
|
|
88
88
|
trust: "unknown",
|
|
89
89
|
firstSeenLabel: "today",
|
|
90
90
|
},
|
|
91
|
-
category: { value: "
|
|
91
|
+
category: { value: "transactional" },
|
|
92
92
|
authenticity: {
|
|
93
93
|
verdict: "caution",
|
|
94
94
|
fromDomain: "1nfomedics.nl",
|
|
@@ -111,7 +111,7 @@ export const Impersonation: Story = {
|
|
|
111
111
|
trust: "unknown",
|
|
112
112
|
firstSeenLabel: "today",
|
|
113
113
|
},
|
|
114
|
-
category: { value: "
|
|
114
|
+
category: { value: "automated" },
|
|
115
115
|
authenticity: {
|
|
116
116
|
verdict: "mismatch",
|
|
117
117
|
fromDomain: "your-bank.example",
|
|
@@ -136,7 +136,7 @@ export const UnreadableSender: Story = {
|
|
|
136
136
|
firstSeenLabel: "today",
|
|
137
137
|
addressUnverified: true,
|
|
138
138
|
},
|
|
139
|
-
category: { value: "
|
|
139
|
+
category: { value: "uncategorized" },
|
|
140
140
|
authenticity: {
|
|
141
141
|
verdict: "mismatch",
|
|
142
142
|
fromDomain: "",
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "lucide-react";
|
|
13
13
|
import type { ReactElement, ReactNode } from "react";
|
|
14
14
|
import { cn } from "../lib/cn.js";
|
|
15
|
+
import { categoryTone, type ThreadCategory } from "./app-shell-types.js";
|
|
15
16
|
import { Avatar } from "./avatar.js";
|
|
16
17
|
import { Badge } from "./badge.js";
|
|
17
18
|
import { Button } from "./button.js";
|
|
@@ -128,7 +129,7 @@ export interface SenderFlagsIntel {
|
|
|
128
129
|
export interface IntelligenceData {
|
|
129
130
|
sender: SenderIntel;
|
|
130
131
|
authenticity: AuthenticityIntel;
|
|
131
|
-
category: { value:
|
|
132
|
+
category: { value: ThreadCategory; overridden?: boolean };
|
|
132
133
|
flags?: SenderFlagsIntel;
|
|
133
134
|
similar: SimilarMessageIntel[];
|
|
134
135
|
}
|
|
@@ -395,7 +396,6 @@ export function IntelligencePanel({
|
|
|
395
396
|
notSpamPending = false,
|
|
396
397
|
}: IntelligencePanelProps) {
|
|
397
398
|
const { sender, authenticity, category, flags = {}, similar } = data;
|
|
398
|
-
const suspicious = authenticity.verdict === "mismatch";
|
|
399
399
|
|
|
400
400
|
return (
|
|
401
401
|
<aside
|
|
@@ -430,9 +430,7 @@ export function IntelligencePanel({
|
|
|
430
430
|
|
|
431
431
|
<Section label="Category">
|
|
432
432
|
<div className="flex items-center gap-2">
|
|
433
|
-
<Badge tone={
|
|
434
|
-
{category.value}
|
|
435
|
-
</Badge>
|
|
433
|
+
<Badge tone={categoryTone[category.value]}>{category.value}</Badge>
|
|
436
434
|
{category.overridden && (
|
|
437
435
|
<span className="text-2xs text-fg-subtle">your override</span>
|
|
438
436
|
)}
|