@remit/ui 0.0.90 → 0.0.92

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/ui",
3
- "version": "0.0.90",
3
+ "version": "0.0.92",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -36,6 +36,19 @@ export const FilterMoveWithManageLink: Story = {
36
36
  },
37
37
  };
38
38
 
39
+ /**
40
+ * A user-reported spam message (issue #648). Independent of the classifier/
41
+ * filter-move shapes above: the badge follows the message wherever it now
42
+ * lives, including a report that never moved the message at all (it was
43
+ * already in Junk).
44
+ */
45
+ export const ReportedAsSpam: Story = {
46
+ args: {
47
+ label: "Reported as spam",
48
+ onUndo: () => alert("Undo"),
49
+ },
50
+ };
51
+
39
52
  export const SideBySide: Story = {
40
53
  render: () => (
41
54
  <div className="flex flex-col items-start gap-3">
@@ -13,7 +13,7 @@ const base: IntelligenceData = {
13
13
  verdict: "aligned",
14
14
  fromDomain: "example.com",
15
15
  dkimDomain: "example.com",
16
- summary: "We verified this message was really sent by example.com.",
16
+ summary: "This message was signed by example.com.",
17
17
  },
18
18
  category: { value: "Personal" },
19
19
  similar: [],
@@ -66,10 +66,13 @@ export const SignedButUnrecognised: Story = {
66
66
  authenticity: {
67
67
  verdict: "caution",
68
68
  fromDomain: "serviceupdatebank.atlassian.net",
69
+ // Deliberately a different domain than fromDomain: the display-name
70
+ // check compared "InfoMedics" against the sender's own domain, never
71
+ // this one, so the summary must name serviceupdatebank.atlassian.net.
69
72
  dkimDomain: "custmx.one.com",
70
73
  claimedBrand: "InfoMedics",
71
74
  summary:
72
- 'This message really was sent by serviceupdatebank.atlassian.net. The name it shows, "InfoMedics", has nothing to do with that domain. Its links go to betaal-vordering.example.',
75
+ 'The name it shows, "InfoMedics", has nothing to do with serviceupdatebank.atlassian.net. Its links go to betaal-vordering.example.',
73
76
  },
74
77
  },
75
78
  },
@@ -92,7 +95,7 @@ export const SignedButLookalikeName: Story = {
92
95
  dkimDomain: "1nfomedics.nl",
93
96
  claimedBrand: "InfoMedics",
94
97
  summary:
95
- 'This message really was sent by 1nfomedics.nl. The name it shows, "InfoMedics", only looks like that domain.',
98
+ 'The name it shows, "InfoMedics", only looks like 1nfomedics.nl.',
96
99
  },
97
100
  },
98
101
  },
@@ -195,31 +198,70 @@ export const WithSimilarMessages: Story = {
195
198
  };
196
199
 
197
200
  /**
198
- * The spam quick actions are symmetric and mutually exclusive, decided by the
199
- * mailbox the message is in. A message sitting in Junk is offered the way out.
201
+ * The spam quick actions are a contextual pair, decided by whether the message
202
+ * carries a spam report never by the mailbox it happens to sit in, since a
203
+ * report on a message already in Junk (the provider's own filter put it there)
204
+ * is a real, no-op-move case (issue #648). A reportable message offers
205
+ * "Report spam".
200
206
  */
201
- export const InJunk: Story = {
207
+ export const Reportable: Story = {
202
208
  args: {
203
209
  data: base,
204
- actions: { onNotSpam: () => {} },
210
+ actions: { onReportSpam: () => {} },
205
211
  },
206
212
  };
207
213
 
208
- /** The inverse, for a message anywhere else. */
209
- export const OutsideJunk: Story = {
214
+ /**
215
+ * Already reported: "Not spam" (the undo) is offered instead of "Report
216
+ * spam", and the panel names the message as reported. Driven by
217
+ * `actions.onNotSpam` being present, not by `flags.blocked` — a sender can be
218
+ * blocked manually, with no report on this particular message, and that must
219
+ * not read as "you reported this" (issue #648 review).
220
+ */
221
+ export const Reported: Story = {
210
222
  args: {
211
223
  data: base,
212
- actions: { onMarkSpam: () => {} },
224
+ actions: { onNotSpam: () => {} },
213
225
  },
214
226
  };
215
227
 
216
228
  /**
217
- * The move has been made. Neither action is offered: pressing the one that just
218
- * ran would ask the mail server to move the message to where it already is.
229
+ * Neither action is offered. The panel hides the pair rather than disabling
230
+ * it unlike VIP/Mute/Unsubscribe, which always render and go visibly
231
+ * unavailable with no handler (issue #51). The host's own wiring never
232
+ * actually reaches this: `resolveSpamAction` always returns one of the two,
233
+ * since every message either carries a spam report or doesn't. Kept as a
234
+ * defensive state for a host that doesn't wire the pair at all.
219
235
  */
220
- export const SpamActionSpent: Story = {
236
+ export const SpamActionUnavailable: Story = {
221
237
  args: {
222
238
  data: base,
223
239
  actions: {},
224
240
  },
225
241
  };
242
+
243
+ /**
244
+ * A "Report spam" press in flight. There's no optimistic update for this
245
+ * action (a report against a message already in Junk is a real no-op-move,
246
+ * issue #648), so without a pending label the button gives no visible
247
+ * response at all until the request lands — the dead-button failure the
248
+ * coding standards call the worst outcome. The button stays clickable
249
+ * throughout, same as the undo direction's `isUndoing`: the operation is
250
+ * idempotent, so a second press is safe, never a queued duplicate.
251
+ */
252
+ export const ReportSpamPending: Story = {
253
+ args: {
254
+ data: base,
255
+ actions: { onReportSpam: () => {} },
256
+ reportSpamPending: true,
257
+ },
258
+ };
259
+
260
+ /** The undo direction's equivalent — "Undoing…" while `notSpam` is in flight. */
261
+ export const NotSpamPending: Story = {
262
+ args: {
263
+ data: base,
264
+ actions: { onNotSpam: () => {} },
265
+ notSpamPending: true,
266
+ },
267
+ };
@@ -1,5 +1,4 @@
1
1
  import {
2
- Ban,
3
2
  BellOff,
4
3
  MailCheck,
5
4
  MailX,
@@ -112,6 +111,16 @@ export type SimilarMessageLinkComponent = (
112
111
  export interface SenderFlagsIntel {
113
112
  vip?: boolean;
114
113
  muted?: boolean;
114
+ /**
115
+ * True once the sender is blocked — via a spam report on some message from
116
+ * them, or a manual block in Settings → Senders, which is the only durable
117
+ * manage-and-undo surface for it now that this panel has no `Block` action
118
+ * of its own. Not used to decide the quick-actions pair below: it's a
119
+ * sender-wide flag, and a manual block with no report on THIS message would
120
+ * make "You reported this sender" false (issue #648 review). The pair and
121
+ * that line key off `actions.onNotSpam`/`onReportSpam` instead, which the
122
+ * host derives from this specific message's `Message.spamReport`.
123
+ */
115
124
  blocked?: boolean;
116
125
  unsubscribed?: boolean;
117
126
  }
@@ -127,20 +136,21 @@ export interface IntelligenceData {
127
136
  export interface IntelligenceQuickActions {
128
137
  onToggleVip?: () => void;
129
138
  onToggleMute?: () => void;
130
- /** Block navigates through a confirm dialog — the callback fires post-confirm. */
131
- onToggleBlock?: () => void;
132
139
  onToggleUnsubscribe?: () => void;
133
140
  onReclassify?: () => void;
134
141
  /**
135
- * "Not spam": move the message out of Junk and promote the sender to
136
- * Wellknown (issue #594). Wired only when the message is currently in Junk.
142
+ * "Not spam": undo a spam report clears the sender block and moves the
143
+ * message back where it came from (issue #648). Wired only when the
144
+ * message currently carries a spam report.
137
145
  */
138
146
  onNotSpam?: () => void;
139
147
  /**
140
- * "Mark spam": move the message into Junk and strip the sender's trust
141
- * (the inverse of "Not spam"). Wired only when the message is not in Junk.
148
+ * "Report spam": move the message into Junk and block the sender (the
149
+ * inverse of "Not spam", issue #648). Wired only when the message has not
150
+ * been reported yet. Replaces the former separate `Block` and `Mark spam`
151
+ * actions with one contextual control.
142
152
  */
143
- onMarkSpam?: () => void;
153
+ onReportSpam?: () => void;
144
154
  }
145
155
 
146
156
  /**
@@ -173,6 +183,19 @@ export interface IntelligencePanelProps {
173
183
  * Drawer header), so there is exactly one way back (#874).
174
184
  */
175
185
  hideCloseButton?: boolean;
186
+ /**
187
+ * True while a "Report spam" press is in flight. There is no optimistic
188
+ * update for this action (issue #648: the client can't predict whether a
189
+ * report against a message already in Junk is a no-op), so without this
190
+ * the button gives no visible response at all until the request lands — a
191
+ * dead button is the worst outcome. Swaps the label to "Reporting…"; the
192
+ * handler stays wired throughout (same as `AutoMovedBadge`'s `isUndoing`)
193
+ * because the operation is idempotent, so a second press mid-flight is
194
+ * safe rather than a queued duplicate.
195
+ */
196
+ reportSpamPending?: boolean;
197
+ /** True while a "Not spam" (undo) press is in flight. Same treatment as `reportSpamPending`. */
198
+ notSpamPending?: boolean;
176
199
  }
177
200
 
178
201
  const trustLabel: Record<
@@ -362,6 +385,8 @@ export function IntelligencePanel({
362
385
  similarLinkComponent,
363
386
  className,
364
387
  hideCloseButton = false,
388
+ reportSpamPending = false,
389
+ notSpamPending = false,
365
390
  }: IntelligencePanelProps) {
366
391
  const { sender, authenticity, category, flags = {}, similar } = data;
367
392
  const suspicious = authenticity.verdict === "mismatch";
@@ -434,35 +459,34 @@ export function IntelligencePanel({
434
459
  active={flags.muted}
435
460
  onClick={actions?.onToggleMute}
436
461
  />
437
- <QuickAction
438
- icon={<Ban className="size-3.5" />}
439
- label="Block"
440
- active={flags.blocked}
441
- danger
442
- onClick={actions?.onToggleBlock}
443
- />
444
- <QuickAction
445
- icon={<MailX className="size-3.5" />}
446
- label="Unsubscribe"
447
- active={flags.unsubscribed}
448
- onClick={actions?.onToggleUnsubscribe}
449
- />
450
462
  {actions?.onNotSpam && (
451
463
  <QuickAction
452
464
  icon={<MailCheck className="size-3.5" />}
453
- label="Not spam"
465
+ label={notSpamPending ? "Undoing…" : "Not spam"}
466
+ active
454
467
  onClick={actions.onNotSpam}
455
468
  />
456
469
  )}
457
- {actions?.onMarkSpam && (
470
+ {actions?.onReportSpam && (
458
471
  <QuickAction
459
472
  icon={<ShieldX className="size-3.5" />}
460
- label="Mark spam"
473
+ label={reportSpamPending ? "Reporting…" : "Report spam"}
461
474
  danger
462
- onClick={actions.onMarkSpam}
475
+ onClick={actions.onReportSpam}
463
476
  />
464
477
  )}
478
+ <QuickAction
479
+ icon={<MailX className="size-3.5" />}
480
+ label="Unsubscribe"
481
+ active={flags.unsubscribed}
482
+ onClick={actions?.onToggleUnsubscribe}
483
+ />
465
484
  </div>
485
+ {actions?.onNotSpam && (
486
+ <p className="mt-1.5 text-2xs text-fg-subtle">
487
+ You reported this message as spam
488
+ </p>
489
+ )}
466
490
  </Section>
467
491
 
468
492
  {(similarState !== "ready" || similar.length > 0) && (