@rapidmx/web-client 0.16.0 → 0.17.0

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.
Files changed (184) hide show
  1. package/README.md +36 -28
  2. package/apps/admin/distribution-lists/new/index.tsx +3 -1
  3. package/apps/admin/domains/[uid].tsx +3 -1
  4. package/apps/admin/domains/new/index.tsx +3 -1
  5. package/apps/admin/escrow-scopes/[uid].tsx +3 -1
  6. package/apps/admin/escrow-scopes/new/index.tsx +3 -1
  7. package/apps/admin/index.tsx +3 -1
  8. package/apps/admin/mailboxes/[uid].tsx +3 -1
  9. package/apps/admin/mailboxes/new/index.tsx +3 -1
  10. package/apps/admin/transport-rules/new/index.tsx +3 -1
  11. package/apps/escrow/matters/new/index.tsx +3 -1
  12. package/apps/shared/components/admin/layout/AdminShell.tsx +4 -1
  13. package/apps/shared/components/admin/setup/SetupWizard.tsx +3 -1
  14. package/apps/shared/components/calendar/layout/CalendarShell.tsx +2 -2
  15. package/apps/shared/components/contacts/layout/ContactsShell.tsx +3 -2
  16. package/apps/shared/components/layout/AppShell.tsx +24 -43
  17. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -0
  18. package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
  19. package/apps/shared/components/mail/MailSelectionBar.tsx +19 -15
  20. package/apps/shared/components/mail/MenuButton.tsx +31 -11
  21. package/apps/shared/components/mail/MessageDetailPane.tsx +202 -4
  22. package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
  23. package/apps/shared/components/mail/layout/MailShell.tsx +37 -34
  24. package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
  25. package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
  26. package/apps/shared/components/mail/reading/messageExport.ts +59 -0
  27. package/apps/shared/components/mail/reading/printMessage.ts +99 -0
  28. package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
  29. package/apps/shared/components/settings/layout/SettingsShell.tsx +4 -2
  30. package/apps/shared/components/tasks/layout/TasksShell.tsx +3 -2
  31. package/apps/shared/keyboard/GlobalShortcuts.tsx +1 -1
  32. package/apps/shared/mail/folderOfType.ts +49 -0
  33. package/apps/shared/mail/permanentDelete.ts +160 -0
  34. package/apps/shared/mail/reportNotices.ts +66 -0
  35. package/apps/shared/mail/senderBlocking.ts +141 -0
  36. package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
  37. package/apps/shared/mail/usePermanentDelete.tsx +160 -0
  38. package/apps/shared/navigation/frameContext.tsx +1 -1
  39. package/apps/shared/navigation/index.ts +43 -0
  40. package/apps/shared/navigation/pageTitle.ts +19 -0
  41. package/apps/www/_shell.tsx +82 -0
  42. package/apps/www/calendar/index.tsx +5 -2
  43. package/apps/www/contacts/[uid].tsx +6 -3
  44. package/apps/www/contacts/index.tsx +8 -3
  45. package/apps/www/index.tsx +146 -31
  46. package/apps/www/messages/[uid].tsx +8 -4
  47. package/apps/www/settings/appearance/index.tsx +5 -2
  48. package/apps/www/settings/auto-reply/index.tsx +5 -2
  49. package/apps/www/settings/blocked-senders/index.tsx +303 -0
  50. package/apps/www/settings/encryption/index.tsx +5 -2
  51. package/apps/www/settings/filters/[uid].tsx +5 -2
  52. package/apps/www/settings/filters/index.tsx +5 -2
  53. package/apps/www/settings/filters/new/index.tsx +30 -5
  54. package/apps/www/settings/labels/index.tsx +5 -2
  55. package/apps/www/settings/privacy/index.tsx +5 -2
  56. package/apps/www/settings/profile/index.tsx +5 -2
  57. package/apps/www/settings/read-receipts/index.tsx +5 -2
  58. package/apps/www/settings/sharing/index.tsx +5 -2
  59. package/apps/www/settings/signatures/[uid].tsx +5 -2
  60. package/apps/www/settings/signatures/index.tsx +5 -2
  61. package/apps/www/settings/signatures/new/index.tsx +6 -3
  62. package/apps/www/tasks/index.tsx +7 -2
  63. package/dist/apps/admin/distribution-lists/new/index.js +3 -1
  64. package/dist/apps/admin/domains/[uid].js +3 -1
  65. package/dist/apps/admin/domains/new/index.js +3 -1
  66. package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
  67. package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
  68. package/dist/apps/admin/index.js +3 -1
  69. package/dist/apps/admin/mailboxes/[uid].js +3 -1
  70. package/dist/apps/admin/mailboxes/new/index.js +3 -1
  71. package/dist/apps/admin/transport-rules/new/index.js +3 -1
  72. package/dist/apps/escrow/matters/new/index.js +3 -1
  73. package/dist/apps/shared/components/admin/layout/AdminShell.js +4 -1
  74. package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
  75. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +2 -2
  76. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +3 -2
  77. package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
  78. package/dist/apps/shared/components/layout/AppShell.js +24 -33
  79. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  80. package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
  81. package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
  82. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +9 -4
  83. package/dist/apps/shared/components/mail/MailSelectionBar.js +10 -12
  84. package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
  85. package/dist/apps/shared/components/mail/MenuButton.js +15 -11
  86. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
  87. package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
  88. package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
  89. package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
  90. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +13 -9
  91. package/dist/apps/shared/components/mail/layout/MailShell.js +27 -33
  92. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
  93. package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
  94. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
  95. package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
  96. package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
  97. package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
  98. package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
  99. package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
  100. package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
  101. package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
  102. package/dist/apps/shared/components/settings/layout/SettingsShell.js +4 -2
  103. package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -2
  104. package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
  105. package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
  106. package/dist/apps/shared/mail/folderOfType.js +40 -0
  107. package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
  108. package/dist/apps/shared/mail/permanentDelete.js +127 -0
  109. package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
  110. package/dist/apps/shared/mail/reportNotices.js +47 -0
  111. package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
  112. package/dist/apps/shared/mail/senderBlocking.js +92 -0
  113. package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
  114. package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
  115. package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
  116. package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
  117. package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
  118. package/dist/apps/shared/navigation/index.d.ts +16 -0
  119. package/dist/apps/shared/navigation/index.js +34 -0
  120. package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
  121. package/dist/apps/shared/navigation/pageTitle.js +12 -0
  122. package/dist/apps/www/_shell.d.ts +26 -0
  123. package/dist/apps/www/_shell.js +31 -0
  124. package/dist/apps/www/calendar/index.d.ts +7 -2
  125. package/dist/apps/www/calendar/index.js +4 -2
  126. package/dist/apps/www/contacts/[uid].d.ts +14 -3
  127. package/dist/apps/www/contacts/[uid].js +5 -3
  128. package/dist/apps/www/contacts/index.d.ts +7 -2
  129. package/dist/apps/www/contacts/index.js +6 -4
  130. package/dist/apps/www/index.d.ts +7 -2
  131. package/dist/apps/www/index.js +117 -19
  132. package/dist/apps/www/messages/[uid].d.ts +14 -3
  133. package/dist/apps/www/messages/[uid].js +7 -5
  134. package/dist/apps/www/settings/appearance/index.d.ts +8 -2
  135. package/dist/apps/www/settings/appearance/index.js +4 -2
  136. package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
  137. package/dist/apps/www/settings/auto-reply/index.js +4 -2
  138. package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
  139. package/dist/apps/www/settings/blocked-senders/index.js +134 -0
  140. package/dist/apps/www/settings/encryption/index.d.ts +7 -2
  141. package/dist/apps/www/settings/encryption/index.js +4 -2
  142. package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
  143. package/dist/apps/www/settings/filters/[uid].js +4 -2
  144. package/dist/apps/www/settings/filters/index.d.ts +7 -2
  145. package/dist/apps/www/settings/filters/index.js +4 -2
  146. package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
  147. package/dist/apps/www/settings/filters/new/index.js +28 -5
  148. package/dist/apps/www/settings/labels/index.d.ts +7 -2
  149. package/dist/apps/www/settings/labels/index.js +4 -2
  150. package/dist/apps/www/settings/privacy/index.d.ts +7 -2
  151. package/dist/apps/www/settings/privacy/index.js +4 -2
  152. package/dist/apps/www/settings/profile/index.d.ts +7 -2
  153. package/dist/apps/www/settings/profile/index.js +4 -2
  154. package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
  155. package/dist/apps/www/settings/read-receipts/index.js +4 -2
  156. package/dist/apps/www/settings/sharing/index.d.ts +7 -2
  157. package/dist/apps/www/settings/sharing/index.js +4 -2
  158. package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
  159. package/dist/apps/www/settings/signatures/[uid].js +4 -2
  160. package/dist/apps/www/settings/signatures/index.d.ts +7 -2
  161. package/dist/apps/www/settings/signatures/index.js +4 -2
  162. package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
  163. package/dist/apps/www/settings/signatures/new/index.js +5 -3
  164. package/dist/apps/www/tasks/index.d.ts +7 -2
  165. package/dist/apps/www/tasks/index.js +5 -3
  166. package/package.json +4 -2
  167. package/apps/shared/navigation/AppRouter.tsx +0 -300
  168. package/apps/shared/navigation/idle.ts +0 -45
  169. package/apps/shared/navigation/routerContext.tsx +0 -83
  170. package/apps/shared/navigation/routes.ts +0 -72
  171. package/apps/www/_routedPage.tsx +0 -24
  172. package/apps/www/_routes.ts +0 -37
  173. package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
  174. package/dist/apps/shared/navigation/AppRouter.js +0 -242
  175. package/dist/apps/shared/navigation/idle.d.ts +0 -14
  176. package/dist/apps/shared/navigation/idle.js +0 -43
  177. package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
  178. package/dist/apps/shared/navigation/routerContext.js +0 -56
  179. package/dist/apps/shared/navigation/routes.d.ts +0 -32
  180. package/dist/apps/shared/navigation/routes.js +0 -37
  181. package/dist/apps/www/_routedPage.d.ts +0 -12
  182. package/dist/apps/www/_routedPage.js +0 -19
  183. package/dist/apps/www/_routes.d.ts +0 -11
  184. package/dist/apps/www/_routes.js +0 -31
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useState } from "react";
7
+ import { useRouter } from "@rapidrest/react/client";
7
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
9
  import { deleteDomain, updateDomain } from "@rapidmx/react-shared/admin/domainsApi.js";
9
10
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
@@ -17,6 +18,7 @@ export default function DomainDetailPage(props) {
17
18
  return (_jsx(AdminShell, { ...props, active: "domains", children: _jsx(DomainDetailContent, { uid: props.params.uid }) }));
18
19
  }
19
20
  function DomainDetailContent({ uid }) {
21
+ const { navigate } = useRouter();
20
22
  const [domain, setDomain] = useState(null);
21
23
  const [confirmingDelete, setConfirmingDelete] = useState(false);
22
24
  const [deleting, setDeleting] = useState(false);
@@ -39,7 +41,7 @@ function DomainDetailContent({ uid }) {
39
41
  setDeleteError(null);
40
42
  try {
41
43
  await deleteDomain(domain.uid, domain.version);
42
- window.location.href = "/admin/domains";
44
+ void navigate("/admin/domains");
43
45
  }
44
46
  catch (err) {
45
47
  setDeleteError(err instanceof ApiRequestError ? err.message : "Could not delete this domain.");
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
+ import { useRouter } from "@rapidrest/react/client";
7
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
9
  import { createDomain, listDomains } from "@rapidmx/react-shared/admin/domainsApi.js";
9
10
  import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
@@ -15,6 +16,7 @@ export default function NewDomainPage(props) {
15
16
  return (_jsx(AdminShell, { ...props, active: "domains", children: _jsx(NewDomainForm, {}) }));
16
17
  }
17
18
  function NewDomainForm() {
19
+ const { navigate } = useRouter();
18
20
  const [name, setName] = useState("");
19
21
  const [aliasOf, setAliasOf] = useState("");
20
22
  // Only an existing, non-alias domain can be aliased (no chains) - see `Domain.aliasOf`'s own doc comment
@@ -37,7 +39,7 @@ function NewDomainForm() {
37
39
  setSaving(true);
38
40
  try {
39
41
  const domain = await createDomain({ name: name.trim(), aliasOf: aliasOf || undefined });
40
- window.location.href = `/admin/domains/${encodeURIComponent(domain.uid)}`;
42
+ void navigate(`/admin/domains/${encodeURIComponent(domain.uid)}`);
41
43
  }
42
44
  catch (err) {
43
45
  setError(err instanceof ApiRequestError ? err.message : "Could not create the domain.");
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
+ import { useRouter } from "@rapidrest/react/client";
7
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
9
  import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
9
10
  import { deleteEscrowScope, getEscrowScope, updateEscrowScope, } from "@rapidmx/react-shared/admin/escrowScopesApi.js";
@@ -95,6 +96,7 @@ export default function EscrowScopeDetailPage(props) {
95
96
  return (_jsx(AdminShell, { ...props, active: "escrowScopes", children: _jsx(EscrowScopeDetailContent, { uid: props.params.uid, adminUid: props.userUid }) }));
96
97
  }
97
98
  function EscrowScopeDetailContent({ uid, adminUid }) {
99
+ const { navigate } = useRouter();
98
100
  const [original, setOriginal] = useState(null);
99
101
  const [name, setName] = useState("");
100
102
  const [description, setDescription] = useState("");
@@ -198,7 +200,7 @@ function EscrowScopeDetailContent({ uid, adminUid }) {
198
200
  setDeleteError(null);
199
201
  try {
200
202
  await deleteEscrowScope(original.uid, original.version);
201
- window.location.href = "/admin/escrow-scopes";
203
+ void navigate("/admin/escrow-scopes");
202
204
  }
203
205
  catch (err) {
204
206
  // Most commonly a 409 ("referenced by an existing Matter") — see `deleteEscrowScope()`'s own
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useState } from "react";
7
+ import { useRouter } from "@rapidrest/react/client";
7
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
9
  import { createEscrowScope } from "@rapidmx/react-shared/admin/escrowScopesApi.js";
9
10
  import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
@@ -16,6 +17,7 @@ export default function NewEscrowScopePage(props) {
16
17
  return (_jsx(AdminShell, { ...props, active: "escrowScopes", children: _jsx(NewEscrowScopeForm, { adminUid: props.userUid }) }));
17
18
  }
18
19
  function NewEscrowScopeForm({ adminUid }) {
20
+ const { navigate } = useRouter();
19
21
  const [name, setName] = useState("");
20
22
  const [description, setDescription] = useState("");
21
23
  const [keyAndHolders, setKeyAndHolders] = useState(emptyEscrowScopeKeyAndHoldersValue());
@@ -61,7 +63,7 @@ function NewEscrowScopeForm({ adminUid }) {
61
63
  requiredHolders: keyAndHolders.requiredHolders,
62
64
  notifySubjectOnAccess: keyAndHolders.notifySubjectOnAccess,
63
65
  });
64
- window.location.href = `/admin/escrow-scopes/${encodeURIComponent(created.uid)}`;
66
+ void navigate(`/admin/escrow-scopes/${encodeURIComponent(created.uid)}`);
65
67
  }
66
68
  catch (err) {
67
69
  setError(err instanceof ApiRequestError ? err.message : "Could not create the escrow scope.");
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
+ import { useRouter } from "@rapidrest/react/client";
7
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
9
  import { listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
9
10
  import { reopenSetup } from "@rapidmx/react-shared/admin/setupApi.js";
@@ -18,6 +19,7 @@ export default function MailboxesListPage(props) {
18
19
  return (_jsx(AdminShell, { ...props, active: "mailboxes", children: _jsx(MailboxesListContent, {}) }));
19
20
  }
20
21
  function MailboxesListContent() {
22
+ const { navigate } = useRouter();
21
23
  const [page, setPage] = useState(0);
22
24
  const [mailboxes, setMailboxes] = useState([]);
23
25
  const [loading, setLoading] = useState(true);
@@ -38,7 +40,7 @@ function MailboxesListContent() {
38
40
  setReopening(true);
39
41
  try {
40
42
  await reopenSetup();
41
- window.location.href = "/admin/setup";
43
+ void navigate("/admin/setup");
42
44
  }
43
45
  catch (err) {
44
46
  setError(err instanceof ApiRequestError ? err.message : "Could not reopen setup.");
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
+ import { useRouter } from "@rapidrest/react/client";
7
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
9
  import { deleteMailbox, getMailbox, impersonateUser } from "@rapidmx/react-shared/mail/mailApi.js";
9
10
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
@@ -27,6 +28,7 @@ export default function MailboxDetailPage(props) {
27
28
  }
28
29
  function MailboxDetailContent({ uid, impersonationBaseUrl, currentUserUid, }) {
29
30
  const [mailbox, setMailbox] = useState(null);
31
+ const { navigate } = useRouter();
30
32
  const [loading, setLoading] = useState(true);
31
33
  const [error, setError] = useState(null);
32
34
  const [confirmingAccess, setConfirmingAccess] = useState(false);
@@ -76,7 +78,7 @@ function MailboxDetailContent({ uid, impersonationBaseUrl, currentUserUid, }) {
76
78
  try {
77
79
  await deleteMailbox(mailbox.uid, mailbox.version, { erase: eraseData });
78
80
  // The Mailboxes page it goes to lists what a delete without `erase` kept, and how far an erasure has got.
79
- window.location.href = "/admin";
81
+ void navigate("/admin");
80
82
  }
81
83
  catch (err) {
82
84
  // Most commonly a 409 if this mailbox is a custodian on an open legal hold (restapi's own
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRouter } from "@rapidrest/react/client";
2
3
  import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
3
4
  import MailboxCreateForm from "../../../shared/components/admin/settings/MailboxCreateForm.js";
4
5
  export default function NewMailboxPage(props) {
6
+ const { navigate } = useRouter();
5
7
  return (_jsx(AdminShell, { ...props, active: "mailboxes", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "New mailbox" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Choose \"Shared mailbox\" to create a true ownerless mailbox (the Exchange \"shared mailbox\" concept) \u2014 access is then granted entirely to delegates afterward, not to a single owner. Choose \"Owned by a specific person\" and look them up to create a mailbox for a specific user instead." }), _jsx(MailboxCreateForm, { cancelHref: "/admin", onCreated: (mailbox) => {
6
- window.location.href = `/admin/mailboxes/${encodeURIComponent(mailbox.uid)}`;
8
+ void navigate(`/admin/mailboxes/${encodeURIComponent(mailbox.uid)}`);
7
9
  } })] }) }));
8
10
  }
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useState } from "react";
7
+ import { useRouter } from "@rapidrest/react/client";
7
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
9
  import { createTransportRule, } from "@rapidmx/react-shared/admin/transportRulesApi.js";
9
10
  import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
@@ -17,6 +18,7 @@ export default function NewTransportRulePage(props) {
17
18
  return (_jsx(AdminShell, { ...props, active: "transportRules", children: _jsx(NewTransportRuleForm, {}) }));
18
19
  }
19
20
  function NewTransportRuleForm() {
21
+ const { navigate } = useRouter();
20
22
  const [name, setName] = useState("");
21
23
  const [rule, setRule] = useState({
22
24
  enabled: true,
@@ -53,7 +55,7 @@ function NewTransportRuleForm() {
53
55
  setSaving(true);
54
56
  try {
55
57
  const created = await createTransportRule({ name: name.trim(), ...rule });
56
- window.location.href = `/admin/transport-rules/${encodeURIComponent(created.uid)}`;
58
+ void navigate(`/admin/transport-rules/${encodeURIComponent(created.uid)}`);
57
59
  }
58
60
  catch (err) {
59
61
  setError(err instanceof ApiRequestError ? err.message : "Could not create the transport rule.");
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useState } from "react";
7
+ import { useRouter } from "@rapidrest/react/client";
7
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
9
  import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
9
10
  import { createMatter } from "@rapidmx/react-shared/admin/mattersApi.js";
@@ -17,6 +18,7 @@ export default function NewMatterPage(props) {
17
18
  return (_jsx(EscrowShell, { ...props, active: "matters", children: _jsx(NewMatterForm, {}) }));
18
19
  }
19
20
  function NewMatterForm() {
21
+ const { navigate } = useRouter();
20
22
  const [name, setName] = useState("");
21
23
  const [description, setDescription] = useState("");
22
24
  // No dropdown here: `EscrowScope` (BaseEscrowScopeRoute) is trusted-admin-only end to end, so this
@@ -62,7 +64,7 @@ function NewMatterForm() {
62
64
  dateRangeStart: new Date(dateRangeStart).toISOString(),
63
65
  dateRangeEnd: new Date(dateRangeEnd).toISOString(),
64
66
  });
65
- window.location.href = `/escrow/matters/${encodeURIComponent(created.uid)}`;
67
+ void navigate(`/escrow/matters/${encodeURIComponent(created.uid)}`);
66
68
  }
67
69
  catch (err) {
68
70
  setError(err instanceof ApiRequestError ? err.message : "Could not create the matter.");
@@ -5,6 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import "../../../styles/app.css";
7
7
  import { useEffect, useState } from "react";
8
+ import { useRouter } from "@rapidrest/react/client";
8
9
  import { HiOutlineClipboardDocumentList, HiOutlineClock, HiOutlineDocumentArrowDown, HiOutlineDocumentCheck, HiOutlineGlobeAlt, HiOutlineInboxStack, HiOutlineKey, HiOutlineLockClosed, HiOutlinePaintBrush, HiOutlinePuzzlePiece, HiOutlineQueueList, HiOutlineRocketLaunch, HiOutlineShieldCheck, HiOutlineShieldExclamation, HiOutlineUserGroup, HiOutlineWrenchScrewdriver, HiOutlineBars3, } from "react-icons/hi2";
9
10
  import { apiFetch, ApiRequestError } from "@rapidmx/react-shared/util/api.js";
10
11
  import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
@@ -108,6 +109,7 @@ export function adminNavItems(pluginNav) {
108
109
  * 403 `api-103` (elevated, but not an administrator), any other 403, and 401 mean "no administrator access".
109
110
  */
110
111
  export default function AdminShell({ active, userUid, authServerUrl, pluginNav, children }) {
112
+ const { navigate } = useRouter();
111
113
  const [status, setStatus] = useState("checking");
112
114
  const [error, setError] = useState(null);
113
115
  const [menuOpen, setMenuOpen] = useState(false);
@@ -129,7 +131,8 @@ export default function AdminShell({ active, userUid, authServerUrl, pluginNav,
129
131
  if (active !== "setup") {
130
132
  try {
131
133
  if ((await getSetupStatus()).required) {
132
- window.location.href = SETUP_ITEM.href;
134
+ // Replacing this page in the history, which would otherwise send the back button round again.
135
+ void navigate(SETUP_ITEM.href, { replace: true });
133
136
  return;
134
137
  }
135
138
  }
@@ -4,6 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useRef, useState } from "react";
7
+ import { useRouter } from "@rapidrest/react/client";
7
8
  import { HiCheck } from "react-icons/hi2";
8
9
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
9
10
  import { getRetentionPolicy } from "@rapidmx/react-shared/admin/retentionPolicyApi.js";
@@ -49,6 +50,7 @@ function errorMessage(err, fallback) {
49
50
  * except the domain can be passed over and configured later from the admin console.
50
51
  */
51
52
  export default function SetupWizard({ userUid }) {
53
+ const { navigate } = useRouter();
52
54
  const [step, setStep] = useState(null);
53
55
  const [status, setStatus] = useState();
54
56
  const [domains, setDomains] = useState([]);
@@ -141,7 +143,7 @@ export default function SetupWizard({ userUid }) {
141
143
  // Let a step still being recorded land first, so it can't arrive after setup is marked finished.
142
144
  await progress.current.running;
143
145
  await completeSetup();
144
- window.location.href = "/admin";
146
+ void navigate("/admin");
145
147
  }
146
148
  catch (err) {
147
149
  setError(errorMessage(err, "Could not finish setup."));
@@ -10,7 +10,7 @@ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.j
10
10
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
11
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
12
  import AppShell from "../../layout/AppShell.js";
13
- import { useLocationSearch } from "../../../navigation/AppRouter.js";
13
+ import { useLocation } from "@rapidrest/react/client";
14
14
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
15
15
  import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
16
16
  const CalendarShellContext = createContext({
@@ -40,7 +40,7 @@ export default function CalendarShell({ userUid, authServerUrl, impersonating, i
40
40
  const [folderRefreshToken, setFolderRefreshToken] = useState(0);
41
41
  // Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
42
42
  // effect, not during render, so the server render and the hydrating render agree.
43
- const search = useLocationSearch();
43
+ const { search } = useLocation();
44
44
  useEffect(() => {
45
45
  setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
46
46
  }, [search]);
@@ -11,7 +11,8 @@ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.j
11
11
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
12
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
13
13
  import AppShell from "../../layout/AppShell.js";
14
- import { useLocationSearch, useNavigate } from "../../../navigation/AppRouter.js";
14
+ import { useLocation } from "@rapidrest/react/client";
15
+ import { useNavigate } from "../../../navigation/index.js";
15
16
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
16
17
  import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
17
18
  const ContactsShellContext = createContext({ mailboxes: [] });
@@ -36,7 +37,7 @@ export default function ContactsShell({ userUid, authServerUrl, impersonating, i
36
37
  const [drawerOpen, setDrawerOpen] = useState(false);
37
38
  // Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
38
39
  // effect, not during render, so the server render and the hydrating render agree.
39
- const search = useLocationSearch();
40
+ const { search } = useLocation();
40
41
  const navigate = useNavigate();
41
42
  useEffect(() => {
42
43
  setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
@@ -49,12 +49,9 @@ export interface AppDef {
49
49
  export declare const APPS: AppDef[];
50
50
  /** `APPS` followed by the plugins' `appRail` items (generic icon), core ids winning - see `mergePluginNavItems`. */
51
51
  export declare function appRailItems(pluginNav?: PluginNav): NavItem[];
52
- /** What only the persistent frame (`AppRouter`) passes to the chrome it keeps mounted. */
52
+ /** What only the persistent frame (`apps/www/_shell.tsx`) passes to the chrome it keeps mounted. */
53
53
  export interface AppChromeProps extends AppShellProps {
54
- /** Changes whenever the router shows a different page: the chrome then moves focus to the content, scrolls to its top,
55
- * sets the document title and announces the page. Absent outside the router, when nothing does. */
56
- routeKey?: string;
57
- /** The router is loading the next page's code - the content is `aria-busy`. */
54
+ /** The router is loading the next page - the content is `aria-busy`. */
58
55
  busy?: boolean;
59
56
  /** A screen that takes over the window (`FrameTakeover`) is showing: the rail, header, banner and footer are hidden - not
60
57
  * unmounted, so what is inside them (and the page in the content region) keeps its state. */
@@ -66,14 +63,14 @@ export interface AppChromeProps extends AppShellProps {
66
63
  * Each app's own shell (e.g. `MailShell`) renders its own contextual sidebar + content as `children`, inside
67
64
  * the area to the right of the icon rail and below the header.
68
65
  *
69
- * Rendered by `AppRouter`, once, for the life of the page, so that moving between apps replaces only `children` - see
70
- * `AppShell` below for what a page's own shell renders instead.
66
+ * Rendered by the webmail's app shell (`apps/www/_shell.tsx`, the router's persistent client layout), once, for the life of the
67
+ * page, so that moving between apps replaces only `children` - see `AppShell` below for what a page's own shell renders instead.
71
68
  */
72
- export declare function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, routeKey, busy, hideChrome, children, }: PropsWithChildren<AppChromeProps>): React.JSX.Element;
69
+ export declare function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, busy, hideChrome, children, }: PropsWithChildren<AppChromeProps>): React.JSX.Element;
73
70
  /**
74
71
  * What a page's own shell (`MailShell`, `CalendarShell`, ...) renders around its content. Outside the client-side router it
75
- * is the whole `AppChrome`, as it always was. Inside it (`AppRouter`) the chrome is already mounted above the page - and stays
76
- * mounted as the page is replaced - so this is only its children; everything it would have been given comes from the router
77
- * frame instead (the props are the same for every page, and `active` is the route's).
72
+ * is the whole `AppChrome`, as it always was. Inside it (the router's app shell, `apps/www/_shell.tsx`) the chrome is already mounted above the
73
+ * page - and stays mounted as the page is replaced - so this is only its children; everything it would have been given comes from
74
+ * the shell instead (the props are the same for every page, and `active` is the route's).
78
75
  */
79
76
  export default function AppShell(props: PropsWithChildren<AppShellProps>): React.JSX.Element;
@@ -4,8 +4,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import "../../styles/app.css";
7
- import { useEffect, useRef, useState } from "react";
7
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
8
8
  import { HiOutlineCalendarDays, HiOutlineClipboardDocumentList, HiOutlineEnvelope, HiOutlinePuzzlePiece, HiOutlineUsers, } from "react-icons/hi2";
9
+ import { useRouter } from "@rapidrest/react/client";
9
10
  import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
10
11
  import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
11
12
  import { stopImpersonating } from "@rapidmx/react-shared/mail/mailApi.js";
@@ -27,7 +28,7 @@ import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
27
28
  import { mergePluginNavItems } from "../../plugins/pluginNav.js";
28
29
  import { useInAppFrame } from "../../navigation/frameContext.js";
29
30
  import { APP_HREFS } from "../../navigation/appHrefs.js";
30
- import { useNavigate } from "../../navigation/routerContext.js";
31
+ import { useNavigate } from "../../navigation/index.js";
31
32
  import { GlobalShortcuts } from "../../keyboard/GlobalShortcuts.js";
32
33
  import { ShortcutProvider } from "../../keyboard/ShortcutProvider.js";
33
34
  import ShortcutsDialog from "../../keyboard/ShortcutsDialog.js";
@@ -84,10 +85,10 @@ export function appRailItems(pluginNav) {
84
85
  * Each app's own shell (e.g. `MailShell`) renders its own contextual sidebar + content as `children`, inside
85
86
  * the area to the right of the icon rail and below the header.
86
87
  *
87
- * Rendered by `AppRouter`, once, for the life of the page, so that moving between apps replaces only `children` - see
88
- * `AppShell` below for what a page's own shell renders instead.
88
+ * Rendered by the webmail's app shell (`apps/www/_shell.tsx`, the router's persistent client layout), once, for the life of the
89
+ * page, so that moving between apps replaces only `children` - see `AppShell` below for what a page's own shell renders instead.
89
90
  */
90
- export function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, routeKey, busy, hideChrome, children, }) {
91
+ export function AppChrome({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, branding: initialBranding, appearance, pluginNav, busy, hideChrome, children, }) {
91
92
  const [stoppingImpersonation, setStoppingImpersonation] = useState(false);
92
93
  // The keyboard shortcuts dialog: opened by `?`/Ctrl+/ (`GlobalShortcuts`) and by the account menu's item.
93
94
  const [shortcutsOpen, setShortcutsOpen] = useState(false);
@@ -111,6 +112,7 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
111
112
  // the router (`AppShell` rendered as a page's own chrome: tests, plugin pages) it stays off, and a Mail shell owns its connection itself.
112
113
  const inFrame = useInAppFrame();
113
114
  const navigate = useNavigate();
115
+ const { pathname } = useRouter();
114
116
  const mail = useMailConnection({ userUid, enabled: inFrame, open: navigate });
115
117
  // A signing certificate the user asked for is announced when it is issued (or fails), on whichever page they are - see the hook.
116
118
  useSigningEnrollmentWatcher({ userUid, mailboxes: mail.mailboxes, enabled: inFrame });
@@ -210,32 +212,21 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
210
212
  window.location.href = "/admin";
211
213
  }
212
214
  }
213
- // What the persistent frame (`AppRouter`) needs on top of a page that never changes: the document title follows the page, and
214
- // after the router replaces the page the keyboard focus, the scroll position and a screen reader are told about it - a
215
- // page load did all of that by itself. Nothing here runs for a page shown outside the router (`routeKey` is undefined).
216
- const contentRef = useRef(null);
217
- const [announcement, setAnnouncement] = useState("");
218
- const firstRouteKeyRef = useRef(routeKey);
219
- const pageLabel = active === "settings" ? "Settings" : appRailItems(pluginNav).find((app) => app.id === active)?.label;
215
+ // The tab's title in the frame is each page's own (its `title` export: rendered by the server, set again by the router on every navigation),
216
+ // but `useBranding()` above sets it to the branding's title once its fetch answers - a title for a page that has none. The page's is put back:
217
+ // the layout effect reads it before that hook's effect runs, this one (declared after it) writes it back.
218
+ const pageTitleRef = useRef("");
219
+ useLayoutEffect(() => {
220
+ pageTitleRef.current = document.title;
221
+ }, [fetchedBranding?.title]);
220
222
  useEffect(() => {
221
- if (routeKey !== undefined) {
222
- const brand = branding?.title || branding?.companyName || "RapidMX";
223
- document.title = pageLabel ? `${brand}: ${pageLabel}` : brand;
223
+ if (inFrame) {
224
+ document.title = pageTitleRef.current;
224
225
  }
225
- }, [routeKey, pageLabel, branding]);
226
- // After the title effect above, so a page change (which rewrites the title) is followed by the unread count going back in front of it.
227
- useUnreadTitle(inboxUnreadTotal(mail.mailboxFolders, mail.folderCounts.counts), {
228
- enabled: inFrame,
229
- resetKey: `${routeKey}|${pageLabel}|${branding?.title}|${branding?.companyName}`,
230
- });
231
- useEffect(() => {
232
- if (routeKey === undefined || routeKey === firstRouteKeyRef.current) {
233
- return;
234
- }
235
- setAnnouncement(pageLabel ?? "");
236
- contentRef.current?.focus({ preventScroll: true });
237
- window.scrollTo(0, 0);
238
- }, [routeKey]);
226
+ }, [fetchedBranding?.title, inFrame]);
227
+ // The router sets the document's title to the page's own whenever the page changes, which drops the unread count this puts in front
228
+ // of it, so a new pathname is what puts the count back. (A folder change is shallow and keeps the title.)
229
+ useUnreadTitle(inboxUnreadTotal(mail.mailboxFolders, mail.folderCounts.counts), { enabled: inFrame, resetKey: pathname });
239
230
  if (!userUid) {
240
231
  return _jsx("div", { className: "min-h-screen" });
241
232
  }
@@ -260,13 +251,13 @@ export function AppChrome({ active, userUid, authServerUrl, impersonating, imper
260
251
  id === active
261
252
  ? "bg-primary/10 text-primary-dark"
262
253
  : "text-text-muted hover:bg-surface-alt hover:text-text",
263
- ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] })), !hideChrome && _jsx(BottomTabBar, { apps: apps, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [!hideChrome && !customHeader && (_jsxs("header", { ref: headerRef, className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeLabel }), renderUserMenu("down")] })), _jsx(NotificationCenter, {}), _jsx("div", { ref: contentRef, id: "app-content", tabIndex: -1, "aria-busy": busy || undefined, className: ["flex-1 flex min-h-0 outline-none", hideChrome ? "" : "pb-14 md:pb-0"].join(" "), children: children })] })] }), routeKey !== undefined && (_jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement })), !hideChrome && _jsx(FrameBrandingFooter, { parsed: footer, userMenu: menuInFooter ? renderUserMenu("up") : undefined, appTitle: activeLabel })] }) })] }), _jsx(NotificationHistoryDialog, { open: historyOpen, onClose: () => setHistoryOpen(false) })] }) }) }));
254
+ ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] })), !hideChrome && _jsx(BottomTabBar, { apps: apps, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [!hideChrome && !customHeader && (_jsxs("header", { ref: headerRef, className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeLabel }), renderUserMenu("down")] })), _jsx(NotificationCenter, {}), _jsx("div", { id: "app-content", tabIndex: -1, "aria-busy": busy || undefined, className: ["flex-1 flex min-h-0 outline-none", hideChrome ? "" : "pb-14 md:pb-0"].join(" "), children: children })] })] }), !hideChrome && _jsx(FrameBrandingFooter, { parsed: footer, userMenu: menuInFooter ? renderUserMenu("up") : undefined, appTitle: activeLabel })] }) })] }), _jsx(NotificationHistoryDialog, { open: historyOpen, onClose: () => setHistoryOpen(false) })] }) }) }));
264
255
  }
265
256
  /**
266
257
  * What a page's own shell (`MailShell`, `CalendarShell`, ...) renders around its content. Outside the client-side router it
267
- * is the whole `AppChrome`, as it always was. Inside it (`AppRouter`) the chrome is already mounted above the page - and stays
268
- * mounted as the page is replaced - so this is only its children; everything it would have been given comes from the router
269
- * frame instead (the props are the same for every page, and `active` is the route's).
258
+ * is the whole `AppChrome`, as it always was. Inside it (the router's app shell, `apps/www/_shell.tsx`) the chrome is already mounted above the
259
+ * page - and stays mounted as the page is replaced - so this is only its children; everything it would have been given comes from
260
+ * the shell instead (the props are the same for every page, and `active` is the route's).
270
261
  */
271
262
  export default function AppShell(props) {
272
263
  const inFrame = useInAppFrame();
@@ -448,7 +448,7 @@ export default function ConversationThreadPane({ conversation, mailboxUid, selec
448
448
  buttonRef: (node) => {
449
449
  headerRefs.current[uid] = node;
450
450
  },
451
- }, footer: uid === footerUid, shortcuts: !!shortcuts && uid === keyboardUid, message: message, attachments: attachmentsByUid[uid] ?? [], isSentItems: folderTypeOf(message) === "sent_items", isOutbox: folderTypeOf(message) === "outbox", draftsFolderUid: folders.find((folder) => folder.type === "drafts")?.uid, folders: folders, onMoved: removeMessage, onFolderCreated: onFolderCreated, onRecalled: patchMessage, onReceiptHandled: patchMessage, onScheduledSendCanceled: removeMessage, onArchived: removeMessage, labels: labels, onLabelsChanged: patchMessage, onLabelCreated: onLabelCreated })) : (_jsxs(_Fragment, { children: [_jsx(CollapsedCard, { from: message.from, date: new Date(message.receivedDate).toLocaleString(), preview: message.bodyPreview || (message.encrypted ? _jsx(EncryptedPreview, {}) : ""), unread: messageUnread, senderClassName: senderClass(messageUnread), dateClassName: dateClass(messageUnread), buttonRef: (node) => {
451
+ }, footer: uid === footerUid, shortcuts: !!shortcuts && uid === keyboardUid, message: message, attachments: attachmentsByUid[uid] ?? [], isSentItems: folderTypeOf(message) === "sent_items", isOutbox: folderTypeOf(message) === "outbox", draftsFolderUid: folders.find((folder) => folder.type === "drafts")?.uid, folders: folders, onMoved: removeMessage, onFolderCreated: onFolderCreated, onRecalled: patchMessage, onReceiptHandled: patchMessage, onScheduledSendCanceled: removeMessage, onArchived: removeMessage, onChanged: patchMessage, labels: labels, onLabelsChanged: patchMessage, onLabelCreated: onLabelCreated })) : (_jsxs(_Fragment, { children: [_jsx(CollapsedCard, { from: message.from, date: new Date(message.receivedDate).toLocaleString(), preview: message.bodyPreview || (message.encrypted ? _jsx(EncryptedPreview, {}) : ""), unread: messageUnread, senderClassName: senderClass(messageUnread), dateClassName: dateClass(messageUnread), buttonRef: (node) => {
452
452
  headerRefs.current[uid] = node;
453
453
  }, buttonProps: {
454
454
  onClick: () => toggleExpanded(uid),
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ export interface EmptyFolderBarProps {
3
+ /** The folder the list is showing ("Deleted Items", "Junk Email"). */
4
+ folderName: string;
5
+ /** How many messages it holds, when that is known. */
6
+ count?: number;
7
+ /** Nothing to empty, or not allowed to. */
8
+ disabled: boolean;
9
+ /** Why, shown as the button's tooltip while it is disabled. */
10
+ disabledReason?: string;
11
+ onEmpty: () => void;
12
+ }
13
+ /**
14
+ * Outlook's "Empty folder", at the top of the list of Deleted Items and of Junk Email: the whole folder is permanently deleted, after the
15
+ * page has asked (this only reports the click). A row of its own under the toolbar rather than a toolbar button, so the toolbar keeps the
16
+ * width a phone gives it and the action is never taken for one of the list's arrangement controls.
17
+ */
18
+ export default function EmptyFolderBar({ folderName, count, disabled, disabledReason, onEmpty }: EmptyFolderBarProps): React.JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { HiOutlineTrash } from "react-icons/hi2";
3
+ /**
4
+ * Outlook's "Empty folder", at the top of the list of Deleted Items and of Junk Email: the whole folder is permanently deleted, after the
5
+ * page has asked (this only reports the click). A row of its own under the toolbar rather than a toolbar button, so the toolbar keeps the
6
+ * width a phone gives it and the action is never taken for one of the list's arrangement controls.
7
+ */
8
+ export default function EmptyFolderBar({ folderName, count, disabled, disabledReason, onEmpty }) {
9
+ return (_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-1 border-b border-border text-xs text-text-muted", children: [_jsx("span", { children: count === undefined ? "" : `${count} ${count === 1 ? "item" : "items"}` }), _jsxs("button", { type: "button", onClick: onEmpty, disabled: disabled, title: disabled ? disabledReason : `Permanently delete everything in ${folderName}`, className: "inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-sm text-danger hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent", children: [_jsx(HiOutlineTrash, { size: 16, "aria-hidden": "true", className: "shrink-0" }), `Empty ${folderName}`] })] }));
10
+ }
@@ -40,7 +40,12 @@ export interface MailSelectionBarProps {
40
40
  * prompt, which would be claiming the move simply didn't happen. */
41
41
  onMoveTo: (folderUid: string) => Promise<void>;
42
42
  onReportJunk: () => void;
43
+ /** Delete: moves the selection to Deleted Items - or, when `deletesPermanently`, permanently deletes it (the page asks first). */
43
44
  onDelete: () => void;
45
+ /** The selection is in Deleted Items, so Delete is the permanent one: the button says "Delete permanently" and is styled as the destructive
46
+ * action it is. The page decides (a search over several mailboxes lists messages from more than one folder, so the folder being viewed is not
47
+ * always the answer). */
48
+ deletesPermanently?: boolean;
44
49
  /** A bulk action is in flight - every action is held until it settles, since the next one would send
45
50
  * `version`s the first has already superseded. */
46
51
  busy: boolean;
@@ -58,9 +63,9 @@ export interface MailSelectionBarProps {
58
63
  * select-all/clear, and the bulk actions Outlook offers for a multi-selection - mark read/unread, flag,
59
64
  * archive, move, report junk and delete.
60
65
  *
61
- * "Delete" means *move to Deleted Items*, never a permanent erase: there is no bulk permanent-delete API,
62
- * and the collection-level DELETE truncates the folder rather than removing a selection. An action whose
63
- * target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
66
+ * "Delete" means *move to Deleted Items* - except on messages that are already in Deleted Items (`deletesPermanently`),
67
+ * where it is "Delete permanently": the page confirms, then erases them for good, as Outlook's Delete does there. An
68
+ * action whose target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
64
69
  * of actions doesn't shift around between folders.
65
70
  */
66
- export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, shortcuts, moveDisabledReason, labelsDisabledReason, }: MailSelectionBarProps): React.JSX.Element;
71
+ export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, deletesPermanently, busy, shortcuts, moveDisabledReason, labelsDisabledReason, }: MailSelectionBarProps): React.JSX.Element;
@@ -9,25 +9,23 @@ import MoveToFolderDialog, { MOVE_TARGET_TYPES } from "./MoveToFolderDialog.js";
9
9
  import { ariaKeyShortcuts, withHint } from "../../keyboard/format.js";
10
10
  import { SHORTCUTS } from "../../keyboard/keymap.js";
11
11
  import { useKeyEnvironment } from "../../keyboard/ShortcutProvider.js";
12
- function actionClassName() {
13
- return "px-2 py-1 rounded-md text-sm text-text hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent";
12
+ function actionClassName(destructive = false) {
13
+ return `px-2 py-1 rounded-md text-sm ${destructive ? "text-danger font-semibold" : "text-text"} hover:bg-surface-alt disabled:opacity-50 disabled:hover:bg-transparent`;
14
14
  }
15
15
  /**
16
16
  * The header that replaces the list's own toolbar while select mode is on: how many rows are selected,
17
17
  * select-all/clear, and the bulk actions Outlook offers for a multi-selection - mark read/unread, flag,
18
18
  * archive, move, report junk and delete.
19
19
  *
20
- * "Delete" means *move to Deleted Items*, never a permanent erase: there is no bulk permanent-delete API,
21
- * and the collection-level DELETE truncates the folder rather than removing a selection. An action whose
22
- * target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
20
+ * "Delete" means *move to Deleted Items* - except on messages that are already in Deleted Items (`deletesPermanently`),
21
+ * where it is "Delete permanently": the page confirms, then erases them for good, as Outlook's Delete does there. An
22
+ * action whose target folder is the one already being viewed is disabled with a reason rather than hidden, so the set
23
23
  * of actions doesn't shift around between folders.
24
24
  */
25
- export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, busy, shortcuts, moveDisabledReason, labelsDisabledReason, }) {
25
+ export default function MailSelectionBar({ selected, listed, totals, onSelectAll, onClearSelection, onCancel, folders, currentFolderUid, labels, mailboxUid, onLabelCreated, onFolderCreated, onApplyLabels, onSetRead, onSetFlagged, onArchive, onMoveTo, onReportJunk, onDelete, deletesPermanently = false, busy, shortcuts, moveDisabledReason, labelsDisabledReason, }) {
26
26
  const env = useKeyEnvironment();
27
- /** `title` and `aria-keyshortcuts` for an action the keyboard also does; `reason` (why it is disabled) wins the tooltip. */
28
- const hint = (label, shortcut, reason) => shortcuts
29
- ? { title: reason ?? withHint(label, shortcut, env), "aria-keyshortcuts": ariaKeyShortcuts(shortcut, env) }
30
- : { title: reason };
27
+ /** `title` and `aria-keyshortcuts` for an action the keyboard also does. */
28
+ const hint = (label, shortcut) => shortcuts ? { title: withHint(label, shortcut, env), "aria-keyshortcuts": ariaKeyShortcuts(shortcut, env) } : {};
31
29
  const selectedCount = totals?.selected ?? selected.length;
32
30
  const listedCount = totals?.listed ?? listed.length;
33
31
  // Counted on the rows that were ticked, but *emptied* on the messages: a ticked conversation whose
@@ -44,12 +42,12 @@ export default function MailSelectionBar({ selected, listed, totals, onSelectAll
44
42
  // them is already being in the folder they would move to.
45
43
  const archiveReason = currentType === "archive" ? "These messages are already in Archive" : undefined;
46
44
  const junkReason = currentType === "junk" ? "These messages are already in Junk" : undefined;
47
- const deleteReason = currentType === "deleted_items" ? "These messages are already in Deleted Items" : undefined;
45
+ const deleteLabel = deletesPermanently ? "Delete permanently" : "Delete";
48
46
  // A label every selected message already carries starts ticked; one only some of them carry starts
49
47
  // partially applied, and is left exactly as it is unless the reader touches that row.
50
48
  const appliedToAll = labels.filter((label) => !none && selected.every((m) => m.labelUids?.includes(label.uid)));
51
49
  const appliedToSome = labels.filter((label) => selected.some((m) => m.labelUids?.includes(label.uid)) && !appliedToAll.includes(label));
52
50
  return (_jsxs("div", { className: "border-b border-border", children: [_jsx(MoveToFolderDialog, { open: movePrompt, onClose: () => setMovePrompt(false), mailboxUid: mailboxUid, folders: folders, currentFolderUid: currentFolderUid, count: selected.length, onMove: onMoveTo, onFolderCreated: onFolderCreated }), _jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 bg-surface-alt", children: [_jsxs("span", { "aria-live": "polite", className: "text-sm font-semibold text-text", children: [selectedCount, totals ? ` ${totals.noun}${selectedCount === 1 ? "" : "s"}` : "", " selected"] }), _jsx("button", { type: "button", onClick: onSelectAll, disabled: allSelected || listedCount === 0, className: actionClassName(), children: "Select all" }), _jsx("button", { type: "button", onClick: onClearSelection, disabled: selectedCount === 0, className: actionClassName(), children: "Clear" }), _jsx("button", { type: "button", onClick: onCancel, className: `${actionClassName()} ml-auto`, children: "Cancel" })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-0.5 px-1.5 py-1", children: [_jsx("button", { type: "button", onClick: () => onSetRead(true), disabled: none || busy, className: actionClassName(), ...hint("Mark read", SHORTCUTS.mail.markRead), children: "Mark read" }), _jsx("button", { type: "button", onClick: () => onSetRead(false), disabled: none || busy, className: actionClassName(), ...hint("Mark unread", SHORTCUTS.mail.markUnread), children: "Mark unread" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(true), disabled: none || busy, className: actionClassName(), ...hint("Flag", SHORTCUTS.mail.flag), children: "Flag" }), _jsx("button", { type: "button", onClick: () => onSetFlagged(false), disabled: none || busy, className: actionClassName(), children: "Unflag" }), _jsx("button", { type: "button", onClick: onArchive, disabled: none || busy || !!archiveReason, title: archiveReason, className: actionClassName(), children: "Archive" }), _jsx(LabelMenuButton, { "aria-label": "Apply label", label: "Apply label", className: "text-sm", labels: labels, mailboxUid: mailboxUid, onLabelCreated: onLabelCreated, applied: appliedToAll.map((label) => label.uid), partial: appliedToSome.map((label) => label.uid), onCommit: onApplyLabels, busy: busy, disabled: none || busy || !!labelsDisabledReason, title: labelsDisabledReason, note: selected.length === 1
53
51
  ? "Ticked labels are applied, unticked ones removed."
54
- : "Ticked labels are applied to every selected message and unticked ones removed from all of them; a dash means only some have that label, and leaving it alone keeps it that way.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }), _jsx("button", { type: "button", onClick: () => setMovePrompt(true), disabled: none || busy || !!moveDisabledReason, title: moveDisabledReason, className: actionClassName(), children: "Move to" }), _jsx("button", { type: "button", onClick: onReportJunk, disabled: none || busy || !!junkReason, title: junkReason, className: actionClassName(), children: "Report junk" }), _jsx("button", { type: "button", onClick: onDelete, disabled: none || busy || !!deleteReason, className: actionClassName(), ...hint("Delete", SHORTCUTS.mail.delete, deleteReason), children: "Delete" })] })] }));
52
+ : "Ticked labels are applied to every selected message and unticked ones removed from all of them; a dash means only some have that label, and leaving it alone keeps it that way.", emptyNote: "This mailbox has no labels yet.", commit: { label: "Apply" }, clear: { label: "Remove all labels" } }), _jsx("button", { type: "button", onClick: () => setMovePrompt(true), disabled: none || busy || !!moveDisabledReason, title: moveDisabledReason, className: actionClassName(), children: "Move to" }), _jsx("button", { type: "button", onClick: onReportJunk, disabled: none || busy || !!junkReason, title: junkReason, className: actionClassName(), children: "Report junk" }), _jsx("button", { type: "button", onClick: onDelete, disabled: none || busy, className: actionClassName(deletesPermanently), ...hint(deleteLabel, SHORTCUTS.mail.delete), children: deleteLabel })] })] }));
55
53
  }