@rapidmx/web-client 0.18.0 → 0.20.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 (131) hide show
  1. package/README.md +381 -381
  2. package/apps/admin/branding/index.tsx +39 -39
  3. package/apps/admin/data-requests/index.tsx +482 -482
  4. package/apps/admin/domains/[uid].tsx +166 -166
  5. package/apps/admin/escrow-scopes/[uid].tsx +350 -350
  6. package/apps/admin/index.tsx +129 -129
  7. package/apps/admin/mailboxes/[uid].tsx +271 -271
  8. package/apps/admin/mailboxes/new/index.tsx +30 -30
  9. package/apps/admin/plugins/index.tsx +15 -15
  10. package/apps/admin/retention-policy/index.tsx +39 -39
  11. package/apps/admin/signing-certificates/index.tsx +343 -343
  12. package/apps/escrow/audit-log/index.tsx +196 -196
  13. package/apps/escrow/matters/[uid].tsx +621 -621
  14. package/apps/shared/auth/adminAccess.ts +99 -99
  15. package/apps/shared/components/admin/diagnostics/HostCard.tsx +2 -0
  16. package/apps/shared/components/admin/diagnostics/PressureTiles.tsx +108 -0
  17. package/apps/shared/components/admin/diagnostics/PvcTable.tsx +20 -6
  18. package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +21 -2
  19. package/apps/shared/components/admin/layout/AdminShell.tsx +384 -384
  20. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -238
  21. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
  22. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -194
  23. package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
  24. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
  25. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
  26. package/apps/shared/components/admin/settings/PluginsManager.tsx +2093 -1545
  27. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
  28. package/apps/shared/components/admin/settings/pluginPreferences.ts +34 -0
  29. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
  30. package/apps/shared/components/admin/setup/SetupWizard.tsx +446 -446
  31. package/apps/shared/components/admin/usePagedList.tsx +129 -129
  32. package/apps/shared/components/calendar/EventModal.tsx +206 -206
  33. package/apps/shared/components/calendar/MonthView.tsx +185 -185
  34. package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
  35. package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
  36. package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
  37. package/apps/shared/components/calendar/allDay.ts +124 -124
  38. package/apps/shared/components/contacts/ContactForm.tsx +383 -383
  39. package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -103
  40. package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
  41. package/apps/shared/components/layout/AppShell.tsx +461 -461
  42. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
  43. package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
  44. package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
  45. package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
  46. package/apps/shared/components/layout/UserMenu.tsx +439 -439
  47. package/apps/shared/components/mail/ConversationList.tsx +332 -323
  48. package/apps/shared/components/mail/ConversationThreadPane.tsx +613 -613
  49. package/apps/shared/components/mail/MailSelectionBar.tsx +240 -240
  50. package/apps/shared/components/mail/MenuButton.tsx +404 -404
  51. package/apps/shared/components/mail/MessageDetailPane.tsx +1757 -1757
  52. package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
  53. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
  54. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1681
  55. package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
  56. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
  57. package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
  58. package/apps/shared/components/mail/listPreferences.ts +3 -3
  59. package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -258
  60. package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -79
  61. package/apps/shared/components/mail/reading/messageExport.ts +59 -59
  62. package/apps/shared/components/mail/reading/printMessage.ts +99 -99
  63. package/apps/shared/components/mail/reading/useMessageActions.ts +443 -443
  64. package/apps/shared/components/mail/verificationSeals.ts +125 -125
  65. package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
  66. package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
  67. package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
  68. package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
  69. package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
  70. package/apps/shared/keyboard/dispatch.ts +124 -124
  71. package/apps/shared/keyboard/format.ts +89 -89
  72. package/apps/shared/keyboard/keymap.ts +114 -114
  73. package/apps/shared/keyboard/registry.ts +65 -65
  74. package/apps/shared/keyboard/targets.ts +79 -79
  75. package/apps/shared/mail/folderOfType.ts +49 -49
  76. package/apps/shared/mail/folderTree.ts +143 -143
  77. package/apps/shared/mail/listAllPages.ts +39 -39
  78. package/apps/shared/mail/newMailNotifications.ts +171 -171
  79. package/apps/shared/mail/outbox/sendJob.ts +445 -445
  80. package/apps/shared/mail/outbox/sendOutcomes.ts +155 -155
  81. package/apps/shared/mail/reportNotices.ts +66 -66
  82. package/apps/shared/mail/senderBlocking.ts +141 -141
  83. package/apps/shared/mail/useMailConnection.ts +205 -205
  84. package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
  85. package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -60
  86. package/apps/shared/mail/useMarkMessageRead.ts +47 -47
  87. package/apps/shared/mail/useNewMailNotifications.ts +178 -178
  88. package/apps/shared/notifications/store.ts +560 -560
  89. package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
  90. package/apps/shared/search/localIndexBuilder.ts +481 -481
  91. package/apps/shared/signing/enrollmentStorage.ts +33 -33
  92. package/apps/shared/signing/enrollmentTracker.ts +385 -385
  93. package/apps/shared/signing/enrollmentView.ts +251 -251
  94. package/apps/shared/signing/useNow.ts +19 -19
  95. package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
  96. package/apps/shared/styles/app.css +396 -396
  97. package/apps/www/calendar/index.tsx +581 -581
  98. package/apps/www/contacts/[uid].tsx +112 -112
  99. package/apps/www/index.tsx +3012 -2962
  100. package/apps/www/messages/[uid].tsx +139 -139
  101. package/apps/www/settings/auto-reply/index.tsx +136 -136
  102. package/apps/www/settings/blocked-senders/index.tsx +303 -303
  103. package/apps/www/settings/encryption/index.tsx +1290 -1290
  104. package/apps/www/settings/filters/[uid].tsx +179 -179
  105. package/apps/www/settings/filters/index.tsx +105 -105
  106. package/apps/www/settings/filters/new/index.tsx +165 -165
  107. package/apps/www/settings/labels/index.tsx +207 -207
  108. package/apps/www/settings/privacy/index.tsx +495 -495
  109. package/apps/www/settings/profile/index.tsx +251 -251
  110. package/apps/www/settings/read-receipts/index.tsx +150 -150
  111. package/apps/www/settings/sharing/index.tsx +259 -259
  112. package/apps/www/settings/signatures/[uid].tsx +175 -175
  113. package/apps/www/settings/signatures/index.tsx +91 -91
  114. package/apps/www/settings/signatures/new/index.tsx +138 -138
  115. package/apps/www/tasks/index.tsx +654 -654
  116. package/dist/apps/shared/components/admin/diagnostics/HostCard.js +2 -1
  117. package/dist/apps/shared/components/admin/diagnostics/PressureTiles.d.ts +22 -0
  118. package/dist/apps/shared/components/admin/diagnostics/PressureTiles.js +52 -0
  119. package/dist/apps/shared/components/admin/diagnostics/PvcTable.js +9 -4
  120. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +35 -2
  121. package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
  122. package/dist/apps/shared/components/admin/settings/PluginsManager.js +328 -48
  123. package/dist/apps/shared/components/admin/settings/pluginPreferences.d.ts +2 -0
  124. package/dist/apps/shared/components/admin/settings/pluginPreferences.js +35 -0
  125. package/dist/apps/shared/components/mail/ConversationList.d.ts +10 -3
  126. package/dist/apps/shared/components/mail/ConversationList.js +9 -5
  127. package/dist/apps/shared/components/mail/listPreferences.d.ts +1 -1
  128. package/dist/apps/shared/components/mail/reading/printMessage.js +11 -11
  129. package/dist/apps/shared/styles/app.css +396 -396
  130. package/dist/apps/www/index.js +51 -10
  131. package/package.json +2 -2
@@ -0,0 +1,2 @@
1
+ export declare function getAllowPrerelease(): boolean;
2
+ export declare function setAllowPrerelease(allow: boolean): void;
@@ -0,0 +1,35 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * Whether the Plugins page offers prerelease versions (`1.0.0-beta.2`) of plugins - for an update, in the version lists
7
+ * and in a plugin search. Stored in `localStorage`, so it is a preference of this browser, not a setting the server
8
+ * keeps: it only decides what the administrator is shown, since an administrator can already install any published
9
+ * version by name. Off unless it was turned on.
10
+ *
11
+ * A read that finds nothing, or a `localStorage` that throws (private-browsing/storage-blocked contexts), is off, and a
12
+ * write that fails is swallowed, so a blocked store only means the choice doesn't survive a reload.
13
+ */
14
+ const STORAGE_KEY = "rapidmx:plugins-allow-prerelease";
15
+ export function getAllowPrerelease() {
16
+ try {
17
+ return localStorage.getItem(STORAGE_KEY) === "true";
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ export function setAllowPrerelease(allow) {
24
+ try {
25
+ if (allow) {
26
+ localStorage.setItem(STORAGE_KEY, "true");
27
+ }
28
+ else {
29
+ localStorage.removeItem(STORAGE_KEY);
30
+ }
31
+ }
32
+ catch {
33
+ // Not persisted; the choice still applies until the page is reloaded.
34
+ }
35
+ }
@@ -1,9 +1,16 @@
1
1
  import React from "react";
2
2
  import { Message } from "@rapidmx/react-shared/mail/mailApi.js";
3
3
  import { ConversationSummary } from "@rapidmx/react-shared/mail/conversationsApi.js";
4
+ /** A conversation row of the list. `mailboxUid` is set only by a view that merges several mailboxes ("All Mailboxes"), whose rows are
5
+ * each from a different one; a list of one mailbox's own leaves it out and every row is that mailbox's. */
6
+ export type ListedConversation = ConversationSummary & {
7
+ mailboxUid?: string;
8
+ };
9
+ /** What tells a row apart from the others: the same conversation id in two mailboxes (a message sent to both) is two rows. */
10
+ export declare function conversationRowKey(conversation: ListedConversation): string;
4
11
  export interface ConversationListProps {
5
- conversations: ConversationSummary[];
6
- /** The mailbox the conversations were listed from - `listConversationMessages()` is mailbox-scoped. */
12
+ conversations: ListedConversation[];
13
+ /** The mailbox the conversations were listed from - `listConversationMessages()` is mailbox-scoped. A row that names its own mailbox uses that. */
7
14
  mailboxUid: string;
8
15
  /** The message currently open in the reading pane, so the row standing for it can be marked. */
9
16
  selectedUid: string | null;
@@ -12,7 +19,7 @@ export interface ConversationListProps {
12
19
  * stands for, or the conversation's own `latestMessageUid` for a parent row. The conversation goes
13
20
  * with the uid because the pane shows the whole thread, not just that message.
14
21
  */
15
- onOpenMessage: (conversation: ConversationSummary, uid: string) => void;
22
+ onOpenMessage: (conversation: ListedConversation, uid: string) => void;
16
23
  /** Newer copies of messages this list already fetched - the reading pane marks the message it opens as
17
24
  * read, which this list would otherwise keep showing as unread until the thread is collapsed and
18
25
  * expanded again. */
@@ -13,6 +13,10 @@ import { listConversationMessages } from "@rapidmx/react-shared/mail/conversatio
13
13
  import { ROW_FOCUS_CLASS, UnreadBar, UnreadLabel, dateClass, isUnread, rowClass, senderClass, subjectClass } from "./unreadStyle.js";
14
14
  import SwipeRow from "./SwipeRow.js";
15
15
  import InviteRowChip from "./invite/InviteRowChip.js";
16
+ /** What tells a row apart from the others: the same conversation id in two mailboxes (a message sent to both) is two rows. */
17
+ export function conversationRowKey(conversation) {
18
+ return conversation.mailboxUid ? `${conversation.mailboxUid}\u0000${conversation.conversationId}` : conversation.conversationId;
19
+ }
16
20
  /** Every participant as `Name <address>`, for the tooltip and the accessible text of the "+N" a crowded row folds them into. */
17
21
  function participantList(conversation) {
18
22
  return conversation.participants.map((participant) => formatMailAddress(participant)).join(", ");
@@ -41,7 +45,7 @@ export default function ConversationList({ conversations, mailboxUid, selectedUi
41
45
  setAnswers((prev) => ({ ...prev, [updated.uid]: updated.meetingResponse }));
42
46
  }
43
47
  function toggle(conversation) {
44
- const id = conversation.conversationId;
48
+ const id = conversationRowKey(conversation);
45
49
  setExpanded((prev) => {
46
50
  const next = new Set(prev);
47
51
  if (next.has(id)) {
@@ -61,7 +65,7 @@ export default function ConversationList({ conversations, mailboxUid, selectedUi
61
65
  delete next[id];
62
66
  return next;
63
67
  });
64
- listConversationMessages(mailboxUid, id)
68
+ listConversationMessages(conversation.mailboxUid ?? mailboxUid, conversation.conversationId)
65
69
  .then((loaded) => setMessagesById((prev) => ({ ...prev, [id]: loaded })))
66
70
  .catch((err) => setErrorsById((prev) => ({
67
71
  ...prev,
@@ -77,15 +81,15 @@ export default function ConversationList({ conversations, mailboxUid, selectedUi
77
81
  return _jsx("p", { className: "p-4 text-sm text-text-muted", children: "No conversations in this folder." });
78
82
  }
79
83
  return (_jsx("ul", { className: swipe?.enabled ? "overflow-x-clip" : undefined, children: conversations.map((conversation) => {
80
- const id = conversation.conversationId;
84
+ const id = conversationRowKey(conversation);
81
85
  const isExpanded = expanded.has(id);
82
86
  const unread = conversation.unreadCount > 0;
83
87
  const loaded = messagesById[id];
84
88
  // `listConversationMessages()` answers oldest first; the rows read in whichever sense the
85
89
  // list itself is arranged in. Copied before reversing - the fetched array is cached.
86
90
  const children = loaded && newestFirst ? [...loaded].reverse() : loaded;
87
- const panelId = `conversation-messages-${id}`;
88
- const ticked = selectedConversationIds?.has(id) ?? false;
91
+ const panelId = `conversation-messages-${conversation.mailboxUid ? `${conversation.mailboxUid}-` : ""}${conversation.conversationId}`;
92
+ const ticked = selectedConversationIds?.has(conversation.conversationId) ?? false;
89
93
  return (_jsxs("li", { children: [_jsxs(SwipeRow, { as: "div", enabled: !!swipe?.enabled && !selectMode, onArchive: () => swipe.onArchive(conversation), onMove: () => swipe.onMove(conversation), "data-message-uid": conversation.latestMessageUid, "data-unread": unread ? "true" : undefined, className: rowClass({ unread, selected: conversation.latestMessageUid === selectedUid || ticked }, "flex items-stretch"), children: [_jsx(UnreadBar, { unread: unread }), selectMode && (_jsx("span", { className: "shrink-0 flex items-center pl-3", children: _jsx("input", { type: "checkbox", checked: ticked, onChange: () => onToggleSelected?.(conversation), "aria-label": `Select conversation: ${conversation.subject || "(no subject)"}`, className: "w-4 h-4 accent-primary" }) })), _jsx("button", { type: "button", onClick: () => toggle(conversation), "aria-expanded": isExpanded, "aria-controls": panelId, "aria-label": `${isExpanded ? "Collapse" : "Expand"} conversation: ${conversation.subject || "(no subject)"}`, className: ["shrink-0 px-2 text-text-muted hover:text-text", ROW_FOCUS_CLASS].join(" "), children: isExpanded ? (_jsx(HiChevronDown, { size: 16, "aria-hidden": "true" })) : (_jsx(HiChevronRight, { size: 16, "aria-hidden": "true" })) }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col", children: [_jsxs("button", { type: "button", "data-row-open": true, onClick: () => onOpenMessage(conversation, conversation.latestMessageUid), className: ["w-full text-left pr-4 py-3", ROW_FOCUS_CLASS].join(" "), children: [_jsx(UnreadLabel, { unread: unread }), _jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx(MailAddress, { recipient: conversation.participants[0] ?? conversation.latestFrom, className: ["flex-1", senderClass(unread)].join(" ") }), conversation.participants.length > 1 && (_jsxs("span", { className: "text-xs text-text-muted shrink-0 font-normal", title: participantList(conversation), children: ["+", conversation.participants.length - 1, _jsxs("span", { className: "sr-only", children: [" more: ", participantList(conversation)] })] })), _jsx("span", { className: ["text-xs shrink-0", dateClass(unread)].join(" "), children: new Date(conversation.latestDate).toLocaleDateString() })] }), _jsx("div", { className: ["text-sm truncate", subjectClass(unread)].join(" "), children: conversation.subject || "(no subject)" }), _jsx("div", { className: "text-xs text-text-muted truncate font-normal", children: conversationLooksEncrypted(conversation) ? _jsx(EncryptedPreview, {}) : conversation.latestPreview }), _jsxs("div", { className: "flex items-center gap-2 mt-1 text-xs text-text-muted font-normal", children: [conversation.messageCount > 1 && _jsxs("span", { children: [conversation.messageCount, " messages"] }), unread && conversation.messageCount > 1 && (_jsxs("span", { className: "py-0.5 px-2 rounded-pill bg-primary/15 text-primary-dark font-bold", children: [conversation.unreadCount, " unread"] })), conversation.hasAttachments && (_jsx(HiOutlinePaperClip, { size: 13, "aria-label": "Has attachments" })), conversation.flagged && (_jsx(HiOutlineFlag, { size: 13, "aria-label": "Flagged", className: "text-danger" }))] })] }), _jsx(InviteRowChip, { message: {
90
94
  uid: conversation.latestMessageUid,
91
95
  meetingMethod: conversation.latestMeetingMethod ?? undefined,
@@ -89,7 +89,7 @@ export declare const CONVERSATION_SORT_NOTE = "Conversations are ordered within
89
89
  * order that would mean nothing. Ties break newest first whichever direction is in force, so two rows the
90
90
  * key can't tell apart still read in a stable, familiar order.
91
91
  */
92
- export declare function sortConversations(conversations: ConversationSummary[], sortBy: MessageListSort, sortOrder: MessageSortOrder): ConversationSummary[];
92
+ export declare function sortConversations<T extends ConversationSummary>(conversations: T[], sortBy: MessageListSort, sortOrder: MessageSortOrder): T[];
93
93
  /** The direction the server itself would pick for a key if none were sent (see `MessageSortOrder`) - what
94
94
  * this list switches to when the reader picks a different field, so changing "Date" to "Subject" reads A-Z
95
95
  * rather than inheriting Date's newest-first as a surprising Z-A. */
@@ -1,17 +1,17 @@
1
1
  import { attachmentSource, makeCidResolver } from "./bodyContent.js";
2
2
  import { prepareBodyHtml } from "./bodyHtml.js";
3
3
  import { frameCsp } from "./frameDocument.js";
4
- const PRINT_CSS = `
5
- html{font:14px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#000;background:#fff}
6
- body{margin:0;padding:24px}
7
- h1{font-size:20px;margin:0 0 12px}
8
- dl{margin:0 0 12px;display:grid;grid-template-columns:max-content 1fr;gap:2px 12px}
9
- dt{font-weight:600;color:#444}
10
- dd{margin:0;overflow-wrap:anywhere}
11
- hr{border:0;border-top:1px solid #999;margin:0 0 16px}
12
- #rr-body{overflow-wrap:break-word}
13
- #rr-body img{max-width:100%;height:auto}
14
- #rr-body pre,pre.rr-text{white-space:pre-wrap;font:inherit;margin:0}
4
+ const PRINT_CSS = `
5
+ html{font:14px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;color:#000;background:#fff}
6
+ body{margin:0;padding:24px}
7
+ h1{font-size:20px;margin:0 0 12px}
8
+ dl{margin:0 0 12px;display:grid;grid-template-columns:max-content 1fr;gap:2px 12px}
9
+ dt{font-weight:600;color:#444}
10
+ dd{margin:0;overflow-wrap:anywhere}
11
+ hr{border:0;border-top:1px solid #999;margin:0 0 16px}
12
+ #rr-body{overflow-wrap:break-word}
13
+ #rr-body img{max-width:100%;height:auto}
14
+ #rr-body pre,pre.rr-text{white-space:pre-wrap;font:inherit;margin:0}
15
15
  `;
16
16
  export function escapeHtml(text) {
17
17
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");