@remit/web-client 0.0.99 → 0.0.100
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/MoveToTrigger.tsx +17 -32
- package/src/components/mail/SelectionWizardHost.tsx +10 -11
- package/src/components/mail/SpamRescue.tsx +17 -30
- package/src/components/settings/DeleteFolderDialog.tsx +11 -15
- package/src/hooks/useFolderLabelTranslator.ts +19 -0
- package/src/lib/move-options.test.ts +140 -0
- package/src/lib/move-options.ts +46 -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.100",
|
|
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": {
|
|
@@ -15,9 +15,9 @@ import { Drawer } from "vaul";
|
|
|
15
15
|
import { ErrorState } from "@/components/ui/ErrorState";
|
|
16
16
|
import { useFolderAppointments } from "@/hooks/useArchiveMailbox";
|
|
17
17
|
import { useCreateMailbox } from "@/hooks/useCreateMailbox";
|
|
18
|
+
import { useFolderLabelTranslator } from "@/hooks/useFolderLabelTranslator";
|
|
18
19
|
import { useIsDesktop } from "@/hooks/useMediaQuery";
|
|
19
|
-
import {
|
|
20
|
-
import { buildMoveTargets } from "@/lib/move-targets";
|
|
20
|
+
import { buildMoveOptions } from "@/lib/move-options";
|
|
21
21
|
import { cn } from "@/lib/utils";
|
|
22
22
|
|
|
23
23
|
interface MoveToTriggerProps {
|
|
@@ -71,15 +71,7 @@ export const MoveToTrigger = ({
|
|
|
71
71
|
const triggerLabel = label ?? "Move to folder";
|
|
72
72
|
const popoverId = useId();
|
|
73
73
|
const { t } = useTranslation("mail", { useSuspense: false });
|
|
74
|
-
|
|
75
|
-
// fallback)` shape; i18next's `t` treats the second argument as an options
|
|
76
|
-
// object — passing it raw breaks fallback behavior. Wrap it the same way
|
|
77
|
-
// the sidebar adapter does, memoized so it's a stable `useMemo` dependency.
|
|
78
|
-
const translator = useCallback(
|
|
79
|
-
(key: string, fallback: string): string =>
|
|
80
|
-
t(key, { defaultValue: fallback }),
|
|
81
|
-
[t],
|
|
82
|
-
);
|
|
74
|
+
const translator = useFolderLabelTranslator();
|
|
83
75
|
|
|
84
76
|
const {
|
|
85
77
|
data: mailboxesResponse,
|
|
@@ -98,28 +90,21 @@ export const MoveToTrigger = ({
|
|
|
98
90
|
const folderAppointments = useFolderAppointments(accountId);
|
|
99
91
|
const { createFolder } = useCreateMailbox(accountId);
|
|
100
92
|
|
|
101
|
-
const options = useMemo<MoveMailboxOption[]>(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return targets.map((mailbox) => ({
|
|
108
|
-
id: mailbox.mailboxId,
|
|
109
|
-
label: labelForMailbox(
|
|
110
|
-
mailbox,
|
|
111
|
-
roleMap.get(mailbox.mailboxId),
|
|
93
|
+
const options = useMemo<MoveMailboxOption[]>(
|
|
94
|
+
() =>
|
|
95
|
+
buildMoveOptions({
|
|
96
|
+
mailboxes: mailboxesResponse?.items ?? [],
|
|
97
|
+
folderAppointments,
|
|
98
|
+
currentMailboxId,
|
|
112
99
|
translator,
|
|
113
|
-
),
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
translator,
|
|
122
|
-
]);
|
|
100
|
+
}),
|
|
101
|
+
[
|
|
102
|
+
mailboxesResponse?.items,
|
|
103
|
+
folderAppointments,
|
|
104
|
+
currentMailboxId,
|
|
105
|
+
translator,
|
|
106
|
+
],
|
|
107
|
+
);
|
|
123
108
|
|
|
124
109
|
const handleSelect = useCallback(
|
|
125
110
|
(destinationMailboxId: string) => {
|
|
@@ -41,16 +41,16 @@ import {
|
|
|
41
41
|
useEscalatedActions,
|
|
42
42
|
} from "@/hooks/useEscalatedActions";
|
|
43
43
|
import { useCreateFilter } from "@/hooks/useFilters";
|
|
44
|
+
import { useFolderLabelTranslator } from "@/hooks/useFolderLabelTranslator";
|
|
44
45
|
import { useMatchSample, useSearchMatchSample } from "@/hooks/useMatchSample";
|
|
45
46
|
import { useOrganizeJob } from "@/hooks/useOrganizeJob";
|
|
46
47
|
import { useOrganizeWiden } from "@/hooks/useOrganizeWiden";
|
|
47
48
|
import { useRulePreview } from "@/hooks/useRulePreview";
|
|
48
49
|
import { useSelectedSubjects } from "@/hooks/useSelectedSubjects";
|
|
49
50
|
import type { BulkActionProgress, BulkRunOutcome } from "@/lib/bulk-actions";
|
|
50
|
-
import { getMailboxDisplayName } from "@/lib/folder-roles";
|
|
51
51
|
import { useListHeaderChrome } from "@/lib/list-header-chrome";
|
|
52
52
|
import { useMailContext } from "@/lib/mail-context";
|
|
53
|
-
import {
|
|
53
|
+
import { buildMoveOptions } from "@/lib/move-options";
|
|
54
54
|
import {
|
|
55
55
|
buildWizardDraft,
|
|
56
56
|
canBackApplyDraft,
|
|
@@ -387,17 +387,16 @@ function SelectionWizardSession({
|
|
|
387
387
|
staleTime: Number.POSITIVE_INFINITY,
|
|
388
388
|
});
|
|
389
389
|
const folderAppointments = useFolderAppointments(accountId);
|
|
390
|
+
const translator = useFolderLabelTranslator();
|
|
390
391
|
const mailboxes = useMemo<MoveMailboxOption[]>(
|
|
391
392
|
() =>
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
),
|
|
400
|
-
[mailboxesData?.items, folderAppointments, mailboxId],
|
|
393
|
+
buildMoveOptions({
|
|
394
|
+
mailboxes: mailboxesData?.items ?? [],
|
|
395
|
+
folderAppointments,
|
|
396
|
+
currentMailboxId: mailboxId,
|
|
397
|
+
translator,
|
|
398
|
+
}),
|
|
399
|
+
[mailboxesData?.items, folderAppointments, mailboxId, translator],
|
|
401
400
|
);
|
|
402
401
|
const { createFolder } = useCreateMailbox(accountId);
|
|
403
402
|
|
|
@@ -14,13 +14,12 @@ import {
|
|
|
14
14
|
useRef,
|
|
15
15
|
useState,
|
|
16
16
|
} from "react";
|
|
17
|
-
import { useTranslation } from "react-i18next";
|
|
18
17
|
import {
|
|
19
18
|
useFolderAppointments,
|
|
20
19
|
useInboxMailbox,
|
|
21
20
|
} from "@/hooks/useArchiveMailbox";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
21
|
+
import { useFolderLabelTranslator } from "@/hooks/useFolderLabelTranslator";
|
|
22
|
+
import { buildMoveOptions } from "@/lib/move-options";
|
|
24
23
|
import {
|
|
25
24
|
recordRescueCandidatesSurfaced,
|
|
26
25
|
recordRescueCommitted,
|
|
@@ -51,12 +50,7 @@ export function SpamRescue({
|
|
|
51
50
|
}: SpamRescueProps) {
|
|
52
51
|
const [open, setOpen] = useState(false);
|
|
53
52
|
const telemetry = useTelemetry();
|
|
54
|
-
const
|
|
55
|
-
const translator = useCallback(
|
|
56
|
-
(key: string, fallback: string): string =>
|
|
57
|
-
t(key, { defaultValue: fallback }),
|
|
58
|
-
[t],
|
|
59
|
-
);
|
|
53
|
+
const translator = useFolderLabelTranslator();
|
|
60
54
|
|
|
61
55
|
const { inboxMailboxId } = useInboxMailbox(accountId);
|
|
62
56
|
const folderAppointments = useFolderAppointments(accountId);
|
|
@@ -66,28 +60,21 @@ export function SpamRescue({
|
|
|
66
60
|
staleTime: Infinity,
|
|
67
61
|
});
|
|
68
62
|
|
|
69
|
-
const folders = useMemo<MoveMailboxOption[]>(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return targets.map((mailbox) => ({
|
|
76
|
-
id: mailbox.mailboxId,
|
|
77
|
-
label: labelForMailbox(
|
|
78
|
-
mailbox,
|
|
79
|
-
roleMap.get(mailbox.mailboxId),
|
|
63
|
+
const folders = useMemo<MoveMailboxOption[]>(
|
|
64
|
+
() =>
|
|
65
|
+
buildMoveOptions({
|
|
66
|
+
mailboxes: mailboxesResponse?.items ?? [],
|
|
67
|
+
folderAppointments,
|
|
68
|
+
currentMailboxId,
|
|
80
69
|
translator,
|
|
81
|
-
),
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
translator,
|
|
90
|
-
]);
|
|
70
|
+
}),
|
|
71
|
+
[
|
|
72
|
+
mailboxesResponse?.items,
|
|
73
|
+
folderAppointments,
|
|
74
|
+
currentMailboxId,
|
|
75
|
+
translator,
|
|
76
|
+
],
|
|
77
|
+
);
|
|
91
78
|
|
|
92
79
|
const defaultDestinationId = useMemo(() => {
|
|
93
80
|
if (inboxMailboxId) return inboxMailboxId;
|
|
@@ -13,13 +13,10 @@ import { AlertTriangle, FolderInput, Loader2, Trash2, X } from "lucide-react";
|
|
|
13
13
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
14
14
|
import { useCreateMailbox } from "@/hooks/useCreateMailbox";
|
|
15
15
|
import { useDeleteFolder } from "@/hooks/useDeleteFolder";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
initialStage,
|
|
19
|
-
moveProgressLabel,
|
|
20
|
-
} from "@/lib/delete-folder";
|
|
16
|
+
import { useFolderLabelTranslator } from "@/hooks/useFolderLabelTranslator";
|
|
17
|
+
import { initialStage, moveProgressLabel } from "@/lib/delete-folder";
|
|
21
18
|
import { getMailboxDisplayName } from "@/lib/folder-roles";
|
|
22
|
-
import {
|
|
19
|
+
import { buildMoveOptions } from "@/lib/move-options";
|
|
23
20
|
|
|
24
21
|
interface DeleteFolderDialogProps {
|
|
25
22
|
open: boolean;
|
|
@@ -61,6 +58,7 @@ export function DeleteFolderDialog({
|
|
|
61
58
|
initialStage(folder.messageCount),
|
|
62
59
|
);
|
|
63
60
|
const { createFolder } = useCreateMailbox(accountId);
|
|
61
|
+
const translator = useFolderLabelTranslator();
|
|
64
62
|
const {
|
|
65
63
|
phase,
|
|
66
64
|
progress,
|
|
@@ -90,15 +88,13 @@ export function DeleteFolderDialog({
|
|
|
90
88
|
|
|
91
89
|
const destinations = useMemo<MoveMailboxOption[]>(
|
|
92
90
|
() =>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
})),
|
|
101
|
-
[mailboxes, appointments, folder.mailboxId],
|
|
91
|
+
buildMoveOptions({
|
|
92
|
+
mailboxes,
|
|
93
|
+
folderAppointments: appointments,
|
|
94
|
+
excludeMailboxId: folder.mailboxId,
|
|
95
|
+
translator,
|
|
96
|
+
}),
|
|
97
|
+
[mailboxes, appointments, folder.mailboxId, translator],
|
|
102
98
|
);
|
|
103
99
|
|
|
104
100
|
const handleCreateFolder = async (
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `labelForMailbox` expects a positional `(key, fallback)` translator; i18next's
|
|
6
|
+
* `t` reads its second argument as an options object, which drops the fallback.
|
|
7
|
+
* Memoized so callers can hold it as a `useMemo` dependency.
|
|
8
|
+
*/
|
|
9
|
+
export const useFolderLabelTranslator = (): ((
|
|
10
|
+
key: string,
|
|
11
|
+
fallback: string,
|
|
12
|
+
) => string) => {
|
|
13
|
+
const { t } = useTranslation("mail", { useSuspense: false });
|
|
14
|
+
return useCallback(
|
|
15
|
+
(key: string, fallback: string): string =>
|
|
16
|
+
t(key, { defaultValue: fallback }),
|
|
17
|
+
[t],
|
|
18
|
+
);
|
|
19
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { describe, test } from "node:test";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import type {
|
|
6
|
+
RemitImapFolderAppointment,
|
|
7
|
+
RemitImapMailboxResponse,
|
|
8
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
9
|
+
import { buildMoveOptions } from "./move-options.js";
|
|
10
|
+
|
|
11
|
+
const englishBundle = JSON.parse(
|
|
12
|
+
readFileSync(
|
|
13
|
+
fileURLToPath(
|
|
14
|
+
new URL("../../public/locales/en/mail.json", import.meta.url),
|
|
15
|
+
),
|
|
16
|
+
"utf8",
|
|
17
|
+
),
|
|
18
|
+
) as { sidebar: Record<string, string> };
|
|
19
|
+
|
|
20
|
+
// The picker is handed the same shipped English bundle the app loads, so the
|
|
21
|
+
// expected labels are the product's own, not strings restated by the test.
|
|
22
|
+
const translate = (key: string, fallback: string): string =>
|
|
23
|
+
englishBundle.sidebar[key.replace("sidebar.", "")] ?? fallback;
|
|
24
|
+
|
|
25
|
+
const make = (
|
|
26
|
+
overrides: Partial<RemitImapMailboxResponse> & {
|
|
27
|
+
mailboxId: string;
|
|
28
|
+
fullPath: string;
|
|
29
|
+
},
|
|
30
|
+
): RemitImapMailboxResponse =>
|
|
31
|
+
({
|
|
32
|
+
accountId: "acct-1",
|
|
33
|
+
namespaceType: "personal",
|
|
34
|
+
namespacePrefix: "",
|
|
35
|
+
hierarchyDelimiter: "/",
|
|
36
|
+
messageCount: 0,
|
|
37
|
+
unseenCount: 0,
|
|
38
|
+
deletedCount: 0,
|
|
39
|
+
createdAt: 0,
|
|
40
|
+
updatedAt: 0,
|
|
41
|
+
...overrides,
|
|
42
|
+
}) as RemitImapMailboxResponse;
|
|
43
|
+
|
|
44
|
+
const appoint = (
|
|
45
|
+
role: RemitImapFolderAppointment["role"],
|
|
46
|
+
mailboxId: string,
|
|
47
|
+
): RemitImapFolderAppointment => ({ role, mailboxId });
|
|
48
|
+
|
|
49
|
+
const applePaths = [
|
|
50
|
+
make({ mailboxId: "mb-inbox", fullPath: "INBOX" }),
|
|
51
|
+
make({ mailboxId: "mb-trash", fullPath: "INBOX/Deleted Messages" }),
|
|
52
|
+
make({ mailboxId: "mb-junk", fullPath: "INBOX/Junk" }),
|
|
53
|
+
make({ mailboxId: "mb-receipts", fullPath: "INBOX/Receipts" }),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const appleAppointments = [
|
|
57
|
+
appoint("Inbox", "mb-inbox"),
|
|
58
|
+
appoint("Trash", "mb-trash"),
|
|
59
|
+
appoint("Junk", "mb-junk"),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const labelOf = (
|
|
63
|
+
options: ReturnType<typeof buildMoveOptions>,
|
|
64
|
+
id: string,
|
|
65
|
+
): string | undefined => options.find((option) => option.id === id)?.label;
|
|
66
|
+
|
|
67
|
+
describe("buildMoveOptions", () => {
|
|
68
|
+
test("labels appointed folders by role, not by the provider's leaf", () => {
|
|
69
|
+
const options = buildMoveOptions({
|
|
70
|
+
mailboxes: applePaths,
|
|
71
|
+
folderAppointments: appleAppointments,
|
|
72
|
+
translator: translate,
|
|
73
|
+
});
|
|
74
|
+
assert.equal(labelOf(options, "mb-inbox"), "Inbox");
|
|
75
|
+
assert.equal(labelOf(options, "mb-trash"), "Trash");
|
|
76
|
+
assert.equal(labelOf(options, "mb-junk"), "Spam");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a displayNameOverride wins over the role label", () => {
|
|
80
|
+
const options = buildMoveOptions({
|
|
81
|
+
mailboxes: [
|
|
82
|
+
make({
|
|
83
|
+
mailboxId: "mb-trash",
|
|
84
|
+
fullPath: "INBOX/Deleted Messages",
|
|
85
|
+
displayNameOverride: "Bin",
|
|
86
|
+
}),
|
|
87
|
+
],
|
|
88
|
+
folderAppointments: [appoint("Trash", "mb-trash")],
|
|
89
|
+
translator: translate,
|
|
90
|
+
});
|
|
91
|
+
assert.equal(labelOf(options, "mb-trash"), "Bin");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("an unappointed folder keeps its own name", () => {
|
|
95
|
+
const options = buildMoveOptions({
|
|
96
|
+
mailboxes: applePaths,
|
|
97
|
+
folderAppointments: appleAppointments,
|
|
98
|
+
translator: translate,
|
|
99
|
+
});
|
|
100
|
+
assert.equal(labelOf(options, "mb-receipts"), "Receipts");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("the full provider path stays searchable", () => {
|
|
104
|
+
const options = buildMoveOptions({
|
|
105
|
+
mailboxes: applePaths,
|
|
106
|
+
folderAppointments: appleAppointments,
|
|
107
|
+
translator: translate,
|
|
108
|
+
});
|
|
109
|
+
assert.equal(
|
|
110
|
+
options.find((option) => option.id === "mb-trash")?.searchValue,
|
|
111
|
+
"INBOX/Deleted Messages",
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("the current mailbox is marked and nothing else is", () => {
|
|
116
|
+
const options = buildMoveOptions({
|
|
117
|
+
mailboxes: applePaths,
|
|
118
|
+
folderAppointments: appleAppointments,
|
|
119
|
+
currentMailboxId: "mb-junk",
|
|
120
|
+
translator: translate,
|
|
121
|
+
});
|
|
122
|
+
assert.deepStrictEqual(
|
|
123
|
+
options.filter((option) => option.isCurrent).map((option) => option.id),
|
|
124
|
+
["mb-junk"],
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("an excluded mailbox is not offered as a destination", () => {
|
|
129
|
+
const options = buildMoveOptions({
|
|
130
|
+
mailboxes: applePaths,
|
|
131
|
+
folderAppointments: appleAppointments,
|
|
132
|
+
excludeMailboxId: "mb-receipts",
|
|
133
|
+
translator: translate,
|
|
134
|
+
});
|
|
135
|
+
assert.equal(
|
|
136
|
+
options.some((option) => option.id === "mb-receipts"),
|
|
137
|
+
false,
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RemitImapFolderAppointment,
|
|
3
|
+
RemitImapMailboxResponse,
|
|
4
|
+
} from "@remit/api-http-client/types.gen.ts";
|
|
5
|
+
import type { MoveMailboxOption } from "@remit/ui";
|
|
6
|
+
import { excludeFolder } from "./delete-folder.js";
|
|
7
|
+
import { buildMailboxRoleMap, labelForMailbox } from "./folder-roles.js";
|
|
8
|
+
import { buildMoveTargets } from "./move-targets.js";
|
|
9
|
+
|
|
10
|
+
type Translator = (key: string, fallback: string) => string;
|
|
11
|
+
|
|
12
|
+
interface MoveOptionsInput {
|
|
13
|
+
mailboxes: readonly RemitImapMailboxResponse[];
|
|
14
|
+
folderAppointments: readonly RemitImapFolderAppointment[];
|
|
15
|
+
/** Rendered as a non-selectable row, so the user sees where mail lives now. */
|
|
16
|
+
currentMailboxId?: string;
|
|
17
|
+
/** Dropped entirely — the folder being deleted is not its own destination. */
|
|
18
|
+
excludeMailboxId?: string;
|
|
19
|
+
translator?: Translator;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The single shaping of a mailbox list into Move-to picker options. Labels
|
|
24
|
+
* follow the account's role appointments (RFC 032, #976) and any
|
|
25
|
+
* `displayNameOverride`, so a picker reads `Inbox`/`Trash` rather than the
|
|
26
|
+
* provider's leaf; the full path stays searchable.
|
|
27
|
+
*/
|
|
28
|
+
export const buildMoveOptions = ({
|
|
29
|
+
mailboxes,
|
|
30
|
+
folderAppointments,
|
|
31
|
+
currentMailboxId,
|
|
32
|
+
excludeMailboxId,
|
|
33
|
+
translator,
|
|
34
|
+
}: MoveOptionsInput): MoveMailboxOption[] => {
|
|
35
|
+
const targets = buildMoveTargets(mailboxes, folderAppointments);
|
|
36
|
+
const roleMap = buildMailboxRoleMap(folderAppointments);
|
|
37
|
+
const destinations = excludeMailboxId
|
|
38
|
+
? excludeFolder(targets, excludeMailboxId)
|
|
39
|
+
: targets;
|
|
40
|
+
return destinations.map((mailbox) => ({
|
|
41
|
+
id: mailbox.mailboxId,
|
|
42
|
+
label: labelForMailbox(mailbox, roleMap.get(mailbox.mailboxId), translator),
|
|
43
|
+
searchValue: mailbox.fullPath,
|
|
44
|
+
isCurrent: mailbox.mailboxId === currentMailboxId,
|
|
45
|
+
}));
|
|
46
|
+
};
|