@remit/web-client 0.0.122 → 0.0.123
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.123",
|
|
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": {
|
|
@@ -61,6 +61,30 @@ describe("resolveSpamAction (#594)", () => {
|
|
|
61
61
|
);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
test("offers nothing once the panel has moved this message", () => {
|
|
65
|
+
assert.equal(
|
|
66
|
+
resolveSpamAction({
|
|
67
|
+
mailboxId: junkId,
|
|
68
|
+
junkMailboxId: junkId,
|
|
69
|
+
inboxMailboxId: inboxId,
|
|
70
|
+
moveApplied: true,
|
|
71
|
+
}),
|
|
72
|
+
null,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("offers Not spam again when the move came back failed", () => {
|
|
77
|
+
assert.equal(
|
|
78
|
+
resolveSpamAction({
|
|
79
|
+
mailboxId: junkId,
|
|
80
|
+
junkMailboxId: junkId,
|
|
81
|
+
inboxMailboxId: inboxId,
|
|
82
|
+
moveApplied: false,
|
|
83
|
+
}),
|
|
84
|
+
"notSpam",
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
64
88
|
test("the two actions are mutually exclusive", () => {
|
|
65
89
|
const inJunk = resolveSpamAction({
|
|
66
90
|
mailboxId: junkId,
|
|
@@ -25,9 +25,9 @@ export interface IntelligencePaneProps {
|
|
|
25
25
|
* the empty-state placeholder.
|
|
26
26
|
*/
|
|
27
27
|
thread?: RemitImapThreadMessageResponse;
|
|
28
|
-
/** The mailbox the message list is currently showing
|
|
28
|
+
/** The mailbox the message list is currently showing. */
|
|
29
29
|
mailboxId?: string;
|
|
30
|
-
/** Account that owns
|
|
30
|
+
/** Account that owns the open message; resolves the Junk/Inbox move targets. */
|
|
31
31
|
accountId?: string;
|
|
32
32
|
/**
|
|
33
33
|
* Hide the panel's own close (X). Set when hosted inside the mobile Drawer,
|
|
@@ -54,12 +54,20 @@ type CategoryOverride = (typeof CATEGORY_OVERRIDES)[number];
|
|
|
54
54
|
/**
|
|
55
55
|
* Decide which spam quick-action to offer for the current message (issue #594).
|
|
56
56
|
*
|
|
57
|
+
* `mailboxId` is the mailbox the MESSAGE is in, which is what the move acts on.
|
|
57
58
|
* The two buttons are symmetric and mutually exclusive:
|
|
58
59
|
* - In the Junk mailbox → offer **Not spam** (move out, promote sender), but
|
|
59
60
|
* only when an Inbox destination is resolved.
|
|
60
61
|
* - Anywhere else → offer **Mark spam** (move in, demote sender), but only when
|
|
61
62
|
* the Junk destination is resolved.
|
|
62
63
|
*
|
|
64
|
+
* `moveApplied` says the panel has already moved this message and the row it is
|
|
65
|
+
* rendering has not caught up. The list drops the row on the move, but the
|
|
66
|
+
* reading pane keeps the message open from the row it was opened with, whose
|
|
67
|
+
* mailbox is still the source — so without this the button survives its own
|
|
68
|
+
* press, and a second press asks the server to move the message to where it now
|
|
69
|
+
* already is.
|
|
70
|
+
*
|
|
63
71
|
* Returns `"notSpam"`, `"markSpam"`, or `null` (no actionable button — e.g. the
|
|
64
72
|
* move source isn't known, or the needed target mailbox hasn't loaded). Pure so
|
|
65
73
|
* the wiring decision can be unit-tested without rendering.
|
|
@@ -68,8 +76,10 @@ export const resolveSpamAction = (input: {
|
|
|
68
76
|
mailboxId?: string;
|
|
69
77
|
junkMailboxId?: string;
|
|
70
78
|
inboxMailboxId?: string;
|
|
79
|
+
moveApplied?: boolean;
|
|
71
80
|
}): "notSpam" | "markSpam" | null => {
|
|
72
|
-
const { mailboxId, junkMailboxId, inboxMailboxId } = input;
|
|
81
|
+
const { mailboxId, junkMailboxId, inboxMailboxId, moveApplied } = input;
|
|
82
|
+
if (moveApplied) return null;
|
|
73
83
|
if (!mailboxId) return null;
|
|
74
84
|
const isInJunk = Boolean(junkMailboxId && mailboxId === junkMailboxId);
|
|
75
85
|
if (isInJunk) return inboxMailboxId ? "notSpam" : null;
|
|
@@ -235,27 +245,39 @@ function WiredPanel({
|
|
|
235
245
|
senderEmail,
|
|
236
246
|
});
|
|
237
247
|
|
|
238
|
-
// "Not spam" / "Mark spam" move the message across the Junk boundary
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
248
|
+
// "Not spam" / "Mark spam" move the message across the Junk boundary. Only
|
|
249
|
+
// one is wired at a time, decided by the mailbox the message itself is in —
|
|
250
|
+
// not by the list the user happens to be looking at, which for a search or a
|
|
251
|
+
// cross-account view names a different folder (issue #594).
|
|
242
252
|
const { junkMailboxId } = useJunkMailbox(accountId);
|
|
243
253
|
const { inboxMailboxId } = useInboxMailbox(accountId);
|
|
244
|
-
const { moveMessages } = useMoveMessages({
|
|
245
|
-
mailboxId: mailboxId
|
|
254
|
+
const { moveMessages, isError: moveFailed } = useMoveMessages({
|
|
255
|
+
mailboxId: thread.mailboxId,
|
|
246
256
|
threadId: thread.threadId,
|
|
247
257
|
accountId,
|
|
248
258
|
});
|
|
249
259
|
const telemetry = useTelemetry();
|
|
260
|
+
const [lastMove, setLastMove] = useState<
|
|
261
|
+
{ messageId: string; mailboxId: string } | undefined
|
|
262
|
+
>(undefined);
|
|
250
263
|
|
|
264
|
+
// Spent while the row this panel is rendering still names the mailbox the
|
|
265
|
+
// move took the message OUT of. It resolves itself: a row that has caught up
|
|
266
|
+
// with the move offers the actions again, and a move that comes back failed
|
|
267
|
+
// is rolled back with a banner, so the retry is against the same button.
|
|
251
268
|
const spamAction = resolveSpamAction({
|
|
252
|
-
mailboxId,
|
|
269
|
+
mailboxId: thread.mailboxId,
|
|
253
270
|
junkMailboxId,
|
|
254
271
|
inboxMailboxId,
|
|
272
|
+
moveApplied:
|
|
273
|
+
!moveFailed &&
|
|
274
|
+
lastMove?.messageId === thread.messageId &&
|
|
275
|
+
lastMove.mailboxId !== thread.mailboxId,
|
|
255
276
|
});
|
|
256
277
|
|
|
257
278
|
const handleNotSpam = useCallback(() => {
|
|
258
279
|
if (!inboxMailboxId) return;
|
|
280
|
+
setLastMove({ messageId: thread.messageId, mailboxId: inboxMailboxId });
|
|
259
281
|
moveMessages([thread.messageId], inboxMailboxId);
|
|
260
282
|
}, [inboxMailboxId, moveMessages, thread.messageId]);
|
|
261
283
|
|
|
@@ -266,6 +288,7 @@ function WiredPanel({
|
|
|
266
288
|
senderTrust: thread.senderTrust,
|
|
267
289
|
wasRescuable: isRescueCandidate(thread),
|
|
268
290
|
});
|
|
291
|
+
setLastMove({ messageId: thread.messageId, mailboxId: junkMailboxId });
|
|
269
292
|
moveMessages([thread.messageId], junkMailboxId);
|
|
270
293
|
}, [junkMailboxId, moveMessages, thread, telemetry]);
|
|
271
294
|
|
|
@@ -71,7 +71,7 @@ export const useMoveMessages = ({
|
|
|
71
71
|
const queryClient = useQueryClient();
|
|
72
72
|
const { pushError } = useErrorBanners();
|
|
73
73
|
|
|
74
|
-
const { mutateAsync, isPending } = useMutation({
|
|
74
|
+
const { mutateAsync, isPending, isError } = useMutation({
|
|
75
75
|
...messageBulkOperationsMoveMessagesMutation(),
|
|
76
76
|
onMutate: async (variables): Promise<MoveContext> => {
|
|
77
77
|
const messageIds = new Set(variables.body.messageIds ?? []);
|
|
@@ -189,5 +189,5 @@ export const useMoveMessages = ({
|
|
|
189
189
|
[mutateAsync],
|
|
190
190
|
);
|
|
191
191
|
|
|
192
|
-
return { moveMessages, isPending };
|
|
192
|
+
return { moveMessages, isPending, isError };
|
|
193
193
|
};
|