@remit/web-client 0.0.252 → 0.0.253
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/MessageCard.tsx +48 -40
- package/src/components/mail/MessageListItem.test.ts +54 -0
- package/src/components/mail/MessageListItem.tsx +3 -0
- package/src/components/mail/MessageSettlementAlert.tsx +68 -0
- package/src/components/mail/SwipeableMessageRow.tsx +10 -2
- package/src/components/mail/conversation-reply-identity.render.test.ts +2 -0
- package/src/components/mail/conversation-reply-reach.render.test.ts +2 -0
- package/src/components/mail/two-window-listeners-one-key.render.test.ts +2 -0
- package/src/hooks/useToggleStar.test.ts +2 -0
- package/src/hooks/useToggleTrusted.test.ts +2 -0
- package/src/lib/brief.test.ts +2 -0
- package/src/lib/brief.ts +2 -0
- package/src/lib/dedupe-thread-messages.test.ts +2 -0
- package/src/lib/drafts.ts +2 -0
- package/src/lib/row-settlement.ts +15 -0
- package/src/lib/thread-search-tokens.test.ts +2 -0
- package/src/test-support/fixtures.ts +2 -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.253",
|
|
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": {
|
|
@@ -17,6 +17,7 @@ import { AutoMovedIndicator } from "./AutoMovedIndicator";
|
|
|
17
17
|
import { MessageActionMenu } from "./MessageActionMenu";
|
|
18
18
|
import { MessageAttachments } from "./MessageAttachments";
|
|
19
19
|
import { MessageBody } from "./MessageBody";
|
|
20
|
+
import { MessageSettlementAlert } from "./MessageSettlementAlert";
|
|
20
21
|
import { MobileMessageBar } from "./MobileMessageBar";
|
|
21
22
|
import { RawMessageView } from "./RawMessageView";
|
|
22
23
|
|
|
@@ -291,46 +292,53 @@ const ExpandedCard = ({
|
|
|
291
292
|
) : undefined
|
|
292
293
|
}
|
|
293
294
|
body={
|
|
294
|
-
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
messageId={threadMessage.messageId}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
295
|
+
<>
|
|
296
|
+
<MessageSettlementAlert
|
|
297
|
+
threadMessage={threadMessage}
|
|
298
|
+
accountId={accountId}
|
|
299
|
+
className="mt-3"
|
|
300
|
+
/>
|
|
301
|
+
{isLoading ? (
|
|
302
|
+
<div className="mt-3 animate-pulse space-y-2">
|
|
303
|
+
<div className="h-4 bg-surface-sunken rounded w-full" />
|
|
304
|
+
<div className="h-4 bg-surface-sunken rounded w-3/4" />
|
|
305
|
+
<div className="h-4 bg-surface-sunken rounded w-1/2" />
|
|
306
|
+
</div>
|
|
307
|
+
) : isError && isMessageNotFoundError(error) ? (
|
|
308
|
+
<div className="mt-3">
|
|
309
|
+
<EmptyState message="This message has been deleted" />
|
|
310
|
+
</div>
|
|
311
|
+
) : isError ? (
|
|
312
|
+
<div className="mt-3">
|
|
313
|
+
<ErrorState
|
|
314
|
+
variant="inline"
|
|
315
|
+
title="Couldn't load this message"
|
|
316
|
+
error={error}
|
|
317
|
+
onRetry={onRetry}
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
) : showRaw ? (
|
|
321
|
+
<div className="mt-3">
|
|
322
|
+
<RawMessageView messageId={threadMessage.messageId} />
|
|
323
|
+
</div>
|
|
324
|
+
) : (
|
|
325
|
+
<div className="mt-3">
|
|
326
|
+
<MessageBody
|
|
327
|
+
bodyParts={messageData?.bodyParts}
|
|
328
|
+
messageId={threadMessage.messageId}
|
|
329
|
+
fromAddressId={messageData?.envelope.from[0]?.addressId}
|
|
330
|
+
isTrusted={isTrusted}
|
|
331
|
+
category={toDisplayCategory(threadMessage.category)}
|
|
332
|
+
/>
|
|
333
|
+
<MessageAttachments
|
|
334
|
+
messageId={threadMessage.messageId}
|
|
335
|
+
bodyParts={messageData?.bodyParts}
|
|
336
|
+
hasAttachment={threadMessage.hasAttachment}
|
|
337
|
+
className="mt-4"
|
|
338
|
+
/>
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
</>
|
|
334
342
|
}
|
|
335
343
|
/>
|
|
336
344
|
);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
4
|
+
import { toImapDraftRowData } from "@/lib/drafts";
|
|
4
5
|
import { threadToRowData } from "./MessageListItem";
|
|
6
|
+
import { swipeableRowData } from "./SwipeableMessageRow";
|
|
5
7
|
|
|
6
8
|
const baseThread = (
|
|
7
9
|
overrides: Partial<RemitImapThreadMessageResponse> = {},
|
|
@@ -41,3 +43,55 @@ describe("threadToRowData — labels", () => {
|
|
|
41
43
|
assert.equal(row.labels, undefined);
|
|
42
44
|
});
|
|
43
45
|
});
|
|
46
|
+
|
|
47
|
+
describe("threadToRowData — settlement (issue #1002)", () => {
|
|
48
|
+
it("marks a row whose delete gave up and came back", () => {
|
|
49
|
+
const row = threadToRowData(
|
|
50
|
+
baseThread({ status: "active", syncStatus: "failed" }),
|
|
51
|
+
);
|
|
52
|
+
assert.equal(row.settlement, "delete_failed");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("says nothing about a move mid-retry, which writes the same failed flag", () => {
|
|
56
|
+
const row = threadToRowData(
|
|
57
|
+
baseThread({ status: "moving", syncStatus: "failed" }),
|
|
58
|
+
);
|
|
59
|
+
assert.equal(row.settlement, undefined);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("says nothing about a delete still being pushed", () => {
|
|
63
|
+
const row = threadToRowData(
|
|
64
|
+
baseThread({ status: "deleting", syncStatus: "pending" }),
|
|
65
|
+
);
|
|
66
|
+
assert.equal(row.settlement, undefined);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("leaves an ordinary inbound row unmarked, pending and all", () => {
|
|
70
|
+
const row = threadToRowData(
|
|
71
|
+
baseThread({ status: "active", syncStatus: "pending" }),
|
|
72
|
+
);
|
|
73
|
+
assert.equal(row.settlement, undefined);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("the mobile and drafts rows carry the same mark", () => {
|
|
78
|
+
it("marks the swipeable mobile row", () => {
|
|
79
|
+
const row = swipeableRowData(
|
|
80
|
+
baseThread({ status: "active", syncStatus: "failed" }),
|
|
81
|
+
);
|
|
82
|
+
assert.equal(row.settlement, "delete_failed");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("marks the drafts row", () => {
|
|
86
|
+
const row = toImapDraftRowData(
|
|
87
|
+
baseThread({ status: "active", syncStatus: "failed" }),
|
|
88
|
+
);
|
|
89
|
+
assert.equal(row.settlement, "delete_failed");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("leaves a move mid-retry unmarked on both", () => {
|
|
93
|
+
const thread = baseThread({ status: "moving", syncStatus: "failed" });
|
|
94
|
+
assert.equal(swipeableRowData(thread).settlement, undefined);
|
|
95
|
+
assert.equal(toImapDraftRowData(thread).settlement, undefined);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* interactive lives in `MessageRow`, which the brief and Flagged render too.
|
|
7
7
|
*/
|
|
8
8
|
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
9
|
+
|
|
9
10
|
import type {
|
|
10
11
|
Density,
|
|
11
12
|
SelectionModifiers,
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
import { memo } from "react";
|
|
16
17
|
import { toDisplayCategory } from "@/lib/display-category";
|
|
17
18
|
import { formatEmailDate } from "@/lib/format";
|
|
19
|
+
import { rowSettlement } from "@/lib/row-settlement";
|
|
18
20
|
import { MessageRow } from "./MessageRow";
|
|
19
21
|
|
|
20
22
|
interface MessageListItemProps {
|
|
@@ -62,6 +64,7 @@ export const threadToRowData = (
|
|
|
62
64
|
// misses.
|
|
63
65
|
suspicious: thread.authenticity?.dkimMismatch === true,
|
|
64
66
|
labels: thread.labels,
|
|
67
|
+
...rowSettlement(thread),
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
const MessageListItemComponent = ({
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
2
|
+
import { hasAbandonedDelete } from "@remit/data-ports/message-settlement";
|
|
3
|
+
import { MessageSettlementNotice, messageSettlementCopy } from "@remit/ui";
|
|
4
|
+
import { useCallback, useMemo } from "react";
|
|
5
|
+
import { useDeleteMessages } from "@/hooks/useDeleteMessages";
|
|
6
|
+
import { buildBugReportContext, buildGitHubIssueUrl } from "@/lib/bug-report";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* States, on the open message, that Remit abandoned its delete (issue #1002) —
|
|
10
|
+
* the failure the list can only chip.
|
|
11
|
+
*
|
|
12
|
+
* The retry is the ordinary delete endpoint, not a new one: abandoning put
|
|
13
|
+
* `status` back to `active`, so `settledPlacement` passes the row through and
|
|
14
|
+
* `POST /messages/delete` re-drives it against the folder the server actually
|
|
15
|
+
* holds it in. Every other give-up gets no treatment — see `hasAbandonedDelete`
|
|
16
|
+
* for the three the two fields cannot tell from a healthy row.
|
|
17
|
+
*/
|
|
18
|
+
export function MessageSettlementAlert({
|
|
19
|
+
threadMessage,
|
|
20
|
+
accountId,
|
|
21
|
+
className,
|
|
22
|
+
}: {
|
|
23
|
+
threadMessage: RemitImapThreadMessageResponse;
|
|
24
|
+
accountId: string | undefined;
|
|
25
|
+
className?: string;
|
|
26
|
+
}) {
|
|
27
|
+
const abandoned = hasAbandonedDelete(threadMessage);
|
|
28
|
+
const { deleteMessages, isPending } = useDeleteMessages({
|
|
29
|
+
mailboxId: threadMessage.mailboxId,
|
|
30
|
+
threadId: threadMessage.threadId,
|
|
31
|
+
accountId,
|
|
32
|
+
messages: [threadMessage],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const onRetry = useCallback(() => {
|
|
36
|
+
deleteMessages([threadMessage.messageId]);
|
|
37
|
+
}, [deleteMessages, threadMessage.messageId]);
|
|
38
|
+
|
|
39
|
+
const reportHref = useMemo(
|
|
40
|
+
() =>
|
|
41
|
+
abandoned
|
|
42
|
+
? buildGitHubIssueUrl(
|
|
43
|
+
buildBugReportContext({
|
|
44
|
+
title: messageSettlementCopy.delete_failed.title,
|
|
45
|
+
errorMessage: `Message ${threadMessage.messageId}: status=${threadMessage.status} syncStatus=${threadMessage.syncStatus}`,
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
: undefined,
|
|
49
|
+
[
|
|
50
|
+
abandoned,
|
|
51
|
+
threadMessage.messageId,
|
|
52
|
+
threadMessage.status,
|
|
53
|
+
threadMessage.syncStatus,
|
|
54
|
+
],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!abandoned) return null;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<MessageSettlementNotice
|
|
61
|
+
settlement="delete_failed"
|
|
62
|
+
onRetry={onRetry}
|
|
63
|
+
retryPending={isPending}
|
|
64
|
+
reportHref={reportHref}
|
|
65
|
+
className={className}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { useCallback, useState } from "react";
|
|
10
10
|
import { toDisplayCategory } from "@/lib/display-category";
|
|
11
11
|
import { formatEmailDate } from "@/lib/format";
|
|
12
|
+
import { rowSettlement } from "@/lib/row-settlement";
|
|
12
13
|
import { useOpenThread } from "@/routing";
|
|
13
14
|
import { MessageListItem } from "./MessageListItem";
|
|
14
15
|
import { useModifierSelect } from "./useModifierSelect";
|
|
@@ -34,7 +35,13 @@ interface SwipeableMessageRowProps {
|
|
|
34
35
|
density?: Density;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The mobile list builds its own row rather than delegating to
|
|
40
|
+
* `MessageListItem` (see the `isDesktop || isMultiSelectMode` branch below),
|
|
41
|
+
* so every row signal has to be repeated here. Exported so a test can hold it
|
|
42
|
+
* to the same answers as the desktop mapper.
|
|
43
|
+
*/
|
|
44
|
+
export const swipeableRowData = (
|
|
38
45
|
thread: RemitImapThreadMessageResponse,
|
|
39
46
|
): ThreadRowData => {
|
|
40
47
|
const suspicious = thread.authenticity?.dkimMismatch === true;
|
|
@@ -52,6 +59,7 @@ const toThreadRowData = (
|
|
|
52
59
|
trust: thread.senderTrust,
|
|
53
60
|
category: toDisplayCategory(thread.category),
|
|
54
61
|
suspicious,
|
|
62
|
+
...rowSettlement(thread),
|
|
55
63
|
};
|
|
56
64
|
};
|
|
57
65
|
|
|
@@ -136,7 +144,7 @@ export const SwipeableMessageRow = ({
|
|
|
136
144
|
onContextMenu={modifierSelect.onContextMenu}
|
|
137
145
|
>
|
|
138
146
|
<SwipeableRow
|
|
139
|
-
thread={
|
|
147
|
+
thread={swipeableRowData(thread)}
|
|
140
148
|
selectionMode={false}
|
|
141
149
|
checked={false}
|
|
142
150
|
active={isSelected}
|
package/src/lib/brief.test.ts
CHANGED
package/src/lib/brief.ts
CHANGED
|
@@ -46,6 +46,7 @@ import type {
|
|
|
46
46
|
} from "@remit/ui";
|
|
47
47
|
import { toDisplayCategory } from "./display-category.js";
|
|
48
48
|
import { formatEmailDate } from "./format.js";
|
|
49
|
+
import { rowSettlement } from "./row-settlement.js";
|
|
49
50
|
import type { SearchToken } from "./search-tokens.js";
|
|
50
51
|
|
|
51
52
|
/**
|
|
@@ -74,6 +75,7 @@ export function toThreadRowData(
|
|
|
74
75
|
trust: thread.senderTrust as SenderTrustLevel,
|
|
75
76
|
category: toDisplayCategory(thread.category),
|
|
76
77
|
suspicious,
|
|
78
|
+
...rowSettlement(thread),
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
81
|
|
package/src/lib/drafts.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
} from "@remit/api-http-client/types.gen.ts";
|
|
27
27
|
import type { ThreadRowData, ThreadSection } from "@remit/ui";
|
|
28
28
|
import { formatEmailDate } from "./format.js";
|
|
29
|
+
import { rowSettlement } from "./row-settlement.js";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Map a Remit outbox draft to the ThreadRowData shape used by remit-ui row
|
|
@@ -65,6 +66,7 @@ export function toImapDraftRowData(
|
|
|
65
66
|
isRead: thread.isRead,
|
|
66
67
|
hasAttachment: thread.hasAttachment,
|
|
67
68
|
starred: thread.hasStars === true,
|
|
69
|
+
...rowSettlement(thread),
|
|
68
70
|
};
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RemitImapThreadMessageResponse } from "@remit/api-http-client/types.gen.ts";
|
|
2
|
+
import { hasAbandonedDelete } from "@remit/data-ports/message-settlement";
|
|
3
|
+
import type { ThreadRowData } from "@remit/ui";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The row treatment for a message whose delete Remit abandoned (issue #1002). The
|
|
7
|
+
* client decides nothing: `hasAbandonedDelete` owns which `status`/`syncStatus`
|
|
8
|
+
* pair proves a terminal give-up, and why no other pair does. Any other row —
|
|
9
|
+
* settled, mid-retry, or an undecidable move — carries no key at all and
|
|
10
|
+
* renders exactly as it always has.
|
|
11
|
+
*/
|
|
12
|
+
export const rowSettlement = (
|
|
13
|
+
thread: Pick<RemitImapThreadMessageResponse, "status" | "syncStatus">,
|
|
14
|
+
): Pick<ThreadRowData, "settlement"> =>
|
|
15
|
+
hasAbandonedDelete(thread) ? { settlement: "delete_failed" } : {};
|