@rapidmx/web-client 0.8.0 → 0.10.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.
- package/README.md +192 -88
- package/apps/shared/auth/accountUrl.ts +9 -0
- package/apps/shared/auth/adminAccess.ts +99 -0
- package/apps/shared/components/admin/elevation.ts +61 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +73 -7
- package/apps/shared/components/admin/settings/BrandingForm.tsx +2 -1
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +87 -24
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
- package/apps/shared/components/contacts/ContactsToolbar.tsx +125 -115
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +15 -3
- package/apps/shared/components/layout/AppShell.tsx +411 -288
- package/apps/shared/components/layout/BrandingChrome.tsx +3 -2
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -126
- package/apps/shared/components/layout/UserMenu.tsx +303 -150
- package/apps/shared/components/mail/ConversationList.tsx +269 -253
- package/apps/shared/components/mail/ConversationThreadPane.tsx +475 -434
- package/apps/shared/components/mail/LazyReadingPane.tsx +115 -0
- package/apps/shared/components/mail/MailAddress.tsx +88 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +235 -222
- package/apps/shared/components/mail/MessageDetailPane.tsx +1458 -1383
- package/apps/shared/components/mail/NewMailToasts.tsx +141 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +279 -160
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1842 -1700
- package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
- package/apps/shared/components/mail/compose/SendFailureAlert.tsx +48 -0
- package/apps/shared/components/mail/compose/composePerf.ts +46 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
- package/apps/shared/components/mail/layout/MailShell.tsx +476 -445
- package/apps/shared/components/mail/unreadStyle.tsx +75 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +252 -240
- package/apps/shared/components/tasks/layout/TasksShell.tsx +15 -3
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
- package/apps/shared/keyboard/dispatch.ts +124 -0
- package/apps/shared/keyboard/format.ts +89 -0
- package/apps/shared/keyboard/keymap.ts +114 -0
- package/apps/shared/keyboard/match.ts +44 -0
- package/apps/shared/keyboard/parse.ts +136 -0
- package/apps/shared/keyboard/platform.ts +34 -0
- package/apps/shared/keyboard/registry.ts +65 -0
- package/apps/shared/keyboard/targets.ts +79 -0
- package/apps/shared/keyboard/useShortcut.ts +50 -0
- package/apps/shared/keyboard/useShortcutProps.ts +17 -0
- package/apps/shared/mail/folderCounts.ts +302 -0
- package/apps/shared/mail/listSnapshots.ts +87 -0
- package/apps/shared/mail/mergeFirstPage.ts +47 -0
- package/apps/shared/mail/messageReadState.ts +85 -0
- package/apps/shared/mail/newMailNotifications.ts +183 -0
- package/apps/shared/mail/useMailConnection.ts +150 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +232 -0
- package/apps/shared/mail/useMarkMessageRead.ts +47 -0
- package/apps/shared/mail/useNewMailNotifications.ts +163 -0
- package/apps/shared/mail/useUnreadTitle.ts +42 -0
- package/apps/shared/navigation/AppRouter.tsx +300 -0
- package/apps/shared/navigation/appHrefs.ts +23 -0
- package/apps/shared/navigation/frameContext.tsx +35 -0
- package/apps/shared/navigation/idle.ts +45 -0
- package/apps/shared/navigation/routerContext.tsx +83 -0
- package/apps/shared/navigation/routes.ts +72 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
- package/apps/shared/styles/app.css +28 -10
- package/apps/www/_routedPage.tsx +24 -0
- package/apps/www/_routes.ts +35 -0
- package/apps/www/calendar/index.tsx +55 -19
- package/apps/www/contacts/[uid].tsx +112 -107
- package/apps/www/contacts/index.tsx +584 -567
- package/apps/www/index.tsx +2280 -1854
- package/apps/www/messages/[uid].tsx +106 -101
- package/apps/www/settings/auto-reply/index.tsx +4 -1
- package/apps/www/settings/encryption/index.tsx +1252 -1249
- package/apps/www/settings/filters/[uid].tsx +4 -1
- package/apps/www/settings/filters/index.tsx +102 -99
- package/apps/www/settings/filters/new/index.tsx +138 -133
- package/apps/www/settings/labels/index.tsx +204 -201
- package/apps/www/settings/privacy/index.tsx +4 -1
- package/apps/www/settings/read-receipts/index.tsx +4 -1
- package/apps/www/settings/sharing/index.tsx +277 -274
- package/apps/www/settings/signatures/[uid].tsx +170 -167
- package/apps/www/settings/signatures/index.tsx +88 -85
- package/apps/www/settings/signatures/new/index.tsx +134 -129
- package/apps/www/tasks/index.tsx +18 -2
- package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
- package/dist/apps/shared/auth/accountUrl.js +8 -0
- package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
- package/dist/apps/shared/auth/adminAccess.js +89 -0
- package/dist/apps/shared/components/admin/elevation.d.ts +24 -0
- package/dist/apps/shared/components/admin/elevation.js +56 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +12 -4
- package/dist/apps/shared/components/admin/layout/AdminShell.js +49 -6
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +1 -1
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +31 -13
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +7 -4
- package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +15 -5
- package/dist/apps/shared/components/layout/AppShell.d.ts +29 -4
- package/dist/apps/shared/components/layout/AppShell.js +73 -14
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +3 -2
- package/dist/apps/shared/components/layout/BrandingChrome.js +3 -2
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
- package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +5 -2
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +45 -8
- package/dist/apps/shared/components/layout/UserMenu.d.ts +28 -9
- package/dist/apps/shared/components/layout/UserMenu.js +87 -14
- package/dist/apps/shared/components/mail/ConversationList.js +9 -13
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +53 -28
- package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
- package/dist/apps/shared/components/mail/LazyReadingPane.js +82 -0
- package/dist/apps/shared/components/mail/MailAddress.d.ts +27 -0
- package/dist/apps/shared/components/mail/MailAddress.js +39 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +4 -1
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -3
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +7 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +87 -36
- package/dist/apps/shared/components/mail/NewMailToasts.d.ts +19 -0
- package/dist/apps/shared/components/mail/NewMailToasts.js +55 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +30 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +71 -5
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +1 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +122 -27
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +14 -0
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +11 -0
- package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
- package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +19 -4
- package/dist/apps/shared/components/mail/layout/MailShell.js +85 -85
- package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
- package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +15 -5
- package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +15 -5
- package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
- package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
- package/dist/apps/shared/keyboard/dispatch.js +109 -0
- package/dist/apps/shared/keyboard/format.d.ts +12 -0
- package/dist/apps/shared/keyboard/format.js +74 -0
- package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
- package/dist/apps/shared/keyboard/keymap.js +84 -0
- package/dist/apps/shared/keyboard/match.d.ts +20 -0
- package/dist/apps/shared/keyboard/match.js +31 -0
- package/dist/apps/shared/keyboard/parse.d.ts +31 -0
- package/dist/apps/shared/keyboard/parse.js +107 -0
- package/dist/apps/shared/keyboard/platform.d.ts +15 -0
- package/dist/apps/shared/keyboard/platform.js +22 -0
- package/dist/apps/shared/keyboard/registry.d.ts +39 -0
- package/dist/apps/shared/keyboard/registry.js +33 -0
- package/dist/apps/shared/keyboard/targets.d.ts +14 -0
- package/dist/apps/shared/keyboard/targets.js +67 -0
- package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
- package/dist/apps/shared/keyboard/useShortcut.js +35 -0
- package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
- package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
- package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
- package/dist/apps/shared/mail/folderCounts.js +212 -0
- package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
- package/dist/apps/shared/mail/listSnapshots.js +43 -0
- package/dist/apps/shared/mail/mergeFirstPage.d.ts +23 -0
- package/dist/apps/shared/mail/mergeFirstPage.js +30 -0
- package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
- package/dist/apps/shared/mail/messageReadState.js +63 -0
- package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
- package/dist/apps/shared/mail/newMailNotifications.js +138 -0
- package/dist/apps/shared/mail/useMailConnection.d.ts +55 -0
- package/dist/apps/shared/mail/useMailConnection.js +96 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +55 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.js +182 -0
- package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
- package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
- package/dist/apps/shared/mail/useNewMailNotifications.d.ts +41 -0
- package/dist/apps/shared/mail/useNewMailNotifications.js +110 -0
- package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
- package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
- package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
- package/dist/apps/shared/navigation/AppRouter.js +242 -0
- package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
- package/dist/apps/shared/navigation/appHrefs.js +20 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
- package/dist/apps/shared/navigation/frameContext.js +24 -0
- package/dist/apps/shared/navigation/idle.d.ts +14 -0
- package/dist/apps/shared/navigation/idle.js +43 -0
- package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
- package/dist/apps/shared/navigation/routerContext.js +56 -0
- package/dist/apps/shared/navigation/routes.d.ts +32 -0
- package/dist/apps/shared/navigation/routes.js +37 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
- package/dist/apps/shared/styles/app.css +28 -10
- package/dist/apps/www/_routedPage.d.ts +12 -0
- package/dist/apps/www/_routedPage.js +19 -0
- package/dist/apps/www/_routes.d.ts +11 -0
- package/dist/apps/www/_routes.js +29 -0
- package/dist/apps/www/calendar/index.d.ts +2 -2
- package/dist/apps/www/calendar/index.js +39 -8
- package/dist/apps/www/contacts/[uid].d.ts +3 -9
- package/dist/apps/www/contacts/[uid].js +6 -2
- package/dist/apps/www/contacts/index.d.ts +2 -2
- package/dist/apps/www/contacts/index.js +17 -4
- package/dist/apps/www/index.d.ts +2 -2
- package/dist/apps/www/index.js +361 -34
- package/dist/apps/www/messages/[uid].d.ts +3 -7
- package/dist/apps/www/messages/[uid].js +7 -4
- package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
- package/dist/apps/www/settings/auto-reply/index.js +3 -1
- package/dist/apps/www/settings/encryption/index.d.ts +2 -2
- package/dist/apps/www/settings/encryption/index.js +3 -1
- package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
- package/dist/apps/www/settings/filters/[uid].js +3 -1
- package/dist/apps/www/settings/filters/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/index.js +3 -1
- package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/new/index.js +6 -2
- package/dist/apps/www/settings/labels/index.d.ts +2 -2
- package/dist/apps/www/settings/labels/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.d.ts +2 -2
- package/dist/apps/www/settings/privacy/index.js +3 -1
- package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
- package/dist/apps/www/settings/read-receipts/index.js +3 -1
- package/dist/apps/www/settings/sharing/index.d.ts +2 -2
- package/dist/apps/www/settings/sharing/index.js +3 -1
- package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
- package/dist/apps/www/settings/signatures/[uid].js +3 -1
- package/dist/apps/www/settings/signatures/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/index.js +3 -1
- package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/new/index.js +6 -2
- package/dist/apps/www/tasks/index.d.ts +2 -2
- package/dist/apps/www/tasks/index.js +15 -3
- package/package.json +2 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect } from "react";
|
|
7
|
+
import { HiOutlineMinus, HiOutlineXMark } from "react-icons/hi2";
|
|
8
|
+
import Skeleton from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
9
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
10
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
11
|
+
import useIsMobile from "@rapidmx/react-shared/util/useIsMobile.js";
|
|
12
|
+
import { markComposePhase } from "./composePerf.js";
|
|
13
|
+
/**
|
|
14
|
+
* What stands in for the compose window in the moment between clicking Compose/Reply/Forward and the window's code being
|
|
15
|
+
* loaded (already there when it was prefetched, so usually not seen at all): the window's own frame at its own size and
|
|
16
|
+
* place, its title and the recipients and subject the window will open with, all of which are known without the network -
|
|
17
|
+
* so the click is answered on the same frame. It is deliberately tiny and lives in the page's own chunk.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here can be typed into yet, and there is no draft to save or discard, so Close just closes.
|
|
20
|
+
*/
|
|
21
|
+
export default function ComposeWindowPlaceholder({ session, failed, onRetry, onClose, onToggleMinimize }) {
|
|
22
|
+
const isMobile = useIsMobile();
|
|
23
|
+
const title = session.initialSubject?.trim() || "New Message";
|
|
24
|
+
const titleId = `compose-title-${session.id}`;
|
|
25
|
+
useEffect(() => markComposePhase(session.id, "shell"), [session.id]);
|
|
26
|
+
if (session.minimized) {
|
|
27
|
+
return (_jsx("div", { role: "dialog", "aria-label": title, className: "w-64 shrink-0 bg-surface border border-border border-b-0 rounded-t-md shadow-modal", children: _jsx("div", { className: "h-10 flex items-center justify-between gap-2 px-3 rounded-t-md bg-primary-darker text-white cursor-pointer", onClick: onToggleMinimize, children: _jsx("span", { className: "text-sm font-medium truncate", children: title }) }) }));
|
|
28
|
+
}
|
|
29
|
+
return (_jsxs("div", { role: "dialog", "aria-labelledby": titleId, "aria-busy": !failed, className: [
|
|
30
|
+
"relative shrink-0 flex flex-col bg-surface border border-border shadow-modal overflow-hidden",
|
|
31
|
+
isMobile ? "fixed inset-0 w-full h-full rounded-none border-0" : "border-b-0 rounded-t-md w-[480px] h-[520px]",
|
|
32
|
+
].join(" "), children: [_jsxs("div", { className: "h-10 shrink-0 flex items-center justify-between gap-2 px-3 bg-primary-darker text-white cursor-pointer", onClick: onToggleMinimize, children: [_jsx("span", { id: titleId, className: "text-sm font-medium truncate", children: title }), _jsxs("div", { className: "flex items-center gap-0.5", onClick: (e) => e.stopPropagation(), children: [_jsx("button", { type: "button", "aria-label": "Minimize", title: "Minimize", onClick: onToggleMinimize, className: "w-6 h-6 flex items-center justify-center rounded-sm text-white/80 hover:bg-white/15 hover:text-white", children: _jsx(HiOutlineMinus, { size: 14 }) }), _jsx("button", { type: "button", "aria-label": "Close", title: "Close", onClick: onClose, className: "w-6 h-6 flex items-center justify-center rounded-sm text-white/80 hover:bg-white/15 hover:text-white", children: _jsx(HiOutlineXMark, { size: 14 }) })] })] }), failed ? (_jsx("div", { className: "px-3 pt-2", children: _jsxs(Alert, { children: [_jsx("p", { className: "mb-2", children: "The compose window couldn\u2019t be loaded. Check your connection and try again." }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: onRetry, children: "Retry" })] }) })) : (_jsxs("div", { role: "status", className: "flex-1 min-h-0 flex flex-col", children: [_jsx("span", { className: "sr-only", children: "Opening the compose window" }), _jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 border-b border-border text-sm", children: [_jsx("span", { className: "text-text-muted", children: "To" }), _jsx("span", { className: "truncate", children: session.initialTo })] }), _jsx("div", { className: "flex items-center gap-2 px-3 py-1.5 border-b border-border text-sm", children: _jsx("span", { className: "truncate", children: session.initialSubject || _jsx("span", { className: "text-text-muted", children: "Subject" }) }) }), _jsxs("div", { className: "flex-1 p-3 flex flex-col gap-2", "aria-hidden": "true", children: [_jsx(Skeleton, { height: "h-4" }), _jsx(Skeleton, { height: "h-4" }), _jsx(Skeleton, { height: "h-4" })] })] }))] }));
|
|
33
|
+
}
|
|
@@ -18,6 +18,14 @@ export interface RichTextEditorProps {
|
|
|
18
18
|
* quote). That normalization happens on the editor's first transaction - even one that only moves the caret -
|
|
19
19
|
* and isn't reported through `onChange`, so a caller can compare later edits against this instead of `value`. */
|
|
20
20
|
onInitialized?: (value: string) => void;
|
|
21
|
+
/**
|
|
22
|
+
* HTML to add at the end of the document, once, as soon as there is some (it may be undefined when the editor is created and
|
|
23
|
+
* defined later). A reply opens its editor before the quoted original has been fetched and hands the quote in through this when
|
|
24
|
+
* it arrives - so whatever has been typed above stays, and the quote lands where it always did, under the signature.
|
|
25
|
+
*/
|
|
26
|
+
appendHtml?: string;
|
|
27
|
+
/** Called with the document as the editor serializes it, right after `appendHtml` was added. */
|
|
28
|
+
onAppended?: (value: string) => void;
|
|
21
29
|
}
|
|
22
30
|
/**
|
|
23
31
|
* A WYSIWYG rich-text editor for the Compose window's message body — replaces `MonacoHtmlEditor` (an HTML
|
|
@@ -40,4 +48,4 @@ export interface RichTextEditorProps {
|
|
|
40
48
|
* sufficient here since TipTap (unlike Monaco) never touches the DOM at module-evaluation time, only when
|
|
41
49
|
* an editor view actually mounts.
|
|
42
50
|
*/
|
|
43
|
-
export default function RichTextEditor({ value, onChange, height, fill, onUploadImage, autoFocusStart, onInitialized, }: RichTextEditorProps): React.JSX.Element;
|
|
51
|
+
export default function RichTextEditor({ value, onChange, height, fill, onUploadImage, autoFocusStart, onInitialized, appendHtml, onAppended, }: RichTextEditorProps): React.JSX.Element;
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { useRef, useState } from "react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import { EditorContent, useEditor } from "@tiptap/react";
|
|
8
8
|
import StarterKit from "@tiptap/starter-kit";
|
|
9
9
|
import { TextStyleKit } from "@tiptap/extension-text-style";
|
|
@@ -34,11 +34,14 @@ import ComposeToolbar from "./ComposeToolbar.js";
|
|
|
34
34
|
* sufficient here since TipTap (unlike Monaco) never touches the DOM at module-evaluation time, only when
|
|
35
35
|
* an editor view actually mounts.
|
|
36
36
|
*/
|
|
37
|
-
export default function RichTextEditor({ value, onChange, height = "360px", fill = false, onUploadImage, autoFocusStart = false, onInitialized, }) {
|
|
37
|
+
export default function RichTextEditor({ value, onChange, height = "360px", fill = false, onUploadImage, autoFocusStart = false, onInitialized, appendHtml, onAppended, }) {
|
|
38
38
|
const onChangeRef = useRef(onChange);
|
|
39
39
|
onChangeRef.current = onChange;
|
|
40
40
|
const onInitializedRef = useRef(onInitialized);
|
|
41
41
|
onInitializedRef.current = onInitialized;
|
|
42
|
+
const onAppendedRef = useRef(onAppended);
|
|
43
|
+
onAppendedRef.current = onAppended;
|
|
44
|
+
const appendedRef = useRef(undefined);
|
|
42
45
|
// Transactions before `create` (the autofocus, and the normalizing one below) aren't edits.
|
|
43
46
|
const initializedRef = useRef(false);
|
|
44
47
|
// Held for this editor's lifetime: TipTap focuses a tick after creating the editor, and a re-render passing a
|
|
@@ -57,6 +60,12 @@ export default function RichTextEditor({ value, onChange, height = "360px", fill
|
|
|
57
60
|
content: value,
|
|
58
61
|
autofocus,
|
|
59
62
|
immediatelyRender: false,
|
|
63
|
+
editorProps: {
|
|
64
|
+
// Ctrl/Cmd+Enter is the compose window's Send. TipTap binds it to a hard break, and a key it handles never reaches the
|
|
65
|
+
// window's shortcut (which skips what a widget already took), so it inserted a line break and sent nothing. Returning true here
|
|
66
|
+
// stops ProseMirror handling it without preventing the event, which goes on to the shortcut. Shift+Enter is still the hard break.
|
|
67
|
+
handleDOMEvents: { keydown: (_view, event) => event.key === "Enter" && (event.ctrlKey || event.metaKey) },
|
|
68
|
+
},
|
|
60
69
|
onCreate: ({ editor: created }) => {
|
|
61
70
|
// An empty transaction runs the schema's append-transaction normalization now, before any edit.
|
|
62
71
|
created.view.dispatch(created.state.tr);
|
|
@@ -69,6 +78,15 @@ export default function RichTextEditor({ value, onChange, height = "360px", fill
|
|
|
69
78
|
}
|
|
70
79
|
},
|
|
71
80
|
});
|
|
81
|
+
// Added once, at the end, whatever the reader has typed meanwhile (the caret stays where it is).
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!editor || appendHtml === undefined || appendedRef.current === appendHtml) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
appendedRef.current = appendHtml;
|
|
87
|
+
editor.commands.insertContentAt(editor.state.doc.content.size, appendHtml);
|
|
88
|
+
onAppendedRef.current?.(editor.getHTML());
|
|
89
|
+
}, [editor, appendHtml]);
|
|
72
90
|
return (_jsxs("div", { className: ["border border-border rounded-sm overflow-hidden", fill ? "h-full flex flex-col" : ""].filter(Boolean).join(" "), children: [_jsx(ComposeToolbar, { editor: editor, onUploadImage: onUploadImage }), _jsx(EditorContent, { editor: editor, style: fill ? undefined : { height }, className: [
|
|
73
91
|
"overflow-y-auto px-3 py-2 text-sm [&_.tiptap]:outline-none [&_.tiptap]:h-full",
|
|
74
92
|
fill ? "flex-1 min-h-0" : "",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface SendFailureAlertProps {
|
|
3
|
+
/** The plain, human message - always shown. */
|
|
4
|
+
message: string;
|
|
5
|
+
/** The technical facts behind it (`describeSendFailure().lines`): per-recipient SMTP results, a transport error.
|
|
6
|
+
* With none, only the message is shown. */
|
|
7
|
+
lines: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The banner a compose window shows when a send (or scheduled send) fails: the plain message, prominent, and - when the
|
|
11
|
+
* server said more - a collapsed "Technical details" block of monospace lines, one per fact, that can be selected or
|
|
12
|
+
* copied in one go to send to whoever runs the server. Collapsed, because most people only need the message.
|
|
13
|
+
*/
|
|
14
|
+
export default function SendFailureAlert({ message, lines }: SendFailureAlertProps): React.JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
3
|
+
import CopyButton from "@rapidmx/react-shared/components/buttons/CopyButton.js";
|
|
4
|
+
/**
|
|
5
|
+
* The banner a compose window shows when a send (or scheduled send) fails: the plain message, prominent, and - when the
|
|
6
|
+
* server said more - a collapsed "Technical details" block of monospace lines, one per fact, that can be selected or
|
|
7
|
+
* copied in one go to send to whoever runs the server. Collapsed, because most people only need the message.
|
|
8
|
+
*/
|
|
9
|
+
export default function SendFailureAlert({ message, lines }) {
|
|
10
|
+
return (_jsx(Alert, { children: _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "font-semibold break-words", children: message }), lines.length > 0 && (_jsxs("details", { className: "mt-2", children: [_jsx("summary", { className: "cursor-pointer text-xs font-semibold", children: "Technical details" }), _jsx("ul", { "aria-label": "Technical details", className: "mt-2 max-h-40 overflow-auto rounded-sm border border-border bg-surface p-2 text-xs font-mono text-text select-text", children: lines.map((line, index) => (_jsx("li", { className: "whitespace-pre-wrap break-words", children: line }, index))) }), _jsx("div", { className: "mt-2", children: _jsx(CopyButton, { value: lines.join("\n"), label: "Copy technical details" }) })] }))] }) }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `performance.mark()`/`measure()` around the phases between clicking Compose/Reply/Forward and the window being usable,
|
|
3
|
+
* for the browser's Performance panel (or `performance.getEntriesByType("measure")`). Marks are named
|
|
4
|
+
* `compose:<window id>:<phase>`; the measures are named `compose:<from>-><to>` and carry the window id in `detail`.
|
|
5
|
+
*
|
|
6
|
+
* Phases, in order: `click` (the handler ran), `shell` (the window's frame is on screen - the placeholder, or the real
|
|
7
|
+
* window once its chunk has loaded), `chunk` (the window's code has loaded and rendered), `body` (the seeded body -
|
|
8
|
+
* signature and quoted original - is ready), `editor` (the editor has mounted and can be typed into), `draft` (the server
|
|
9
|
+
* draft exists).
|
|
10
|
+
*
|
|
11
|
+
* On in development builds, and in a production build when `window.__RAPIDMX_PERF__` is set to `true` (from the console:
|
|
12
|
+
* `window.__RAPIDMX_PERF__ = true`). Off otherwise, when each call is a no-op.
|
|
13
|
+
*/
|
|
14
|
+
export type ComposePhase = "click" | "shell" | "chunk" | "body" | "editor" | "draft";
|
|
15
|
+
/** Marks `phase` of window `id` once (later calls for the same phase are ignored), and measures it from `click`. */
|
|
16
|
+
export declare function markComposePhase(id: string, phase: ComposePhase): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* `performance.mark()`/`measure()` around the phases between clicking Compose/Reply/Forward and the window being usable,
|
|
7
|
+
* for the browser's Performance panel (or `performance.getEntriesByType("measure")`). Marks are named
|
|
8
|
+
* `compose:<window id>:<phase>`; the measures are named `compose:<from>-><to>` and carry the window id in `detail`.
|
|
9
|
+
*
|
|
10
|
+
* Phases, in order: `click` (the handler ran), `shell` (the window's frame is on screen - the placeholder, or the real
|
|
11
|
+
* window once its chunk has loaded), `chunk` (the window's code has loaded and rendered), `body` (the seeded body -
|
|
12
|
+
* signature and quoted original - is ready), `editor` (the editor has mounted and can be typed into), `draft` (the server
|
|
13
|
+
* draft exists).
|
|
14
|
+
*
|
|
15
|
+
* On in development builds, and in a production build when `window.__RAPIDMX_PERF__` is set to `true` (from the console:
|
|
16
|
+
* `window.__RAPIDMX_PERF__ = true`). Off otherwise, when each call is a no-op.
|
|
17
|
+
*/
|
|
18
|
+
function enabled() {
|
|
19
|
+
if (typeof performance === "undefined" || typeof performance.mark !== "function") {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return import.meta.env?.DEV === true || globalThis.__RAPIDMX_PERF__ === true;
|
|
23
|
+
}
|
|
24
|
+
/** The compose sessions a `click` mark was made for, so the first mark of each phase is the only one that counts. */
|
|
25
|
+
const marked = new Set();
|
|
26
|
+
/** Marks `phase` of window `id` once (later calls for the same phase are ignored), and measures it from `click`. */
|
|
27
|
+
export function markComposePhase(id, phase) {
|
|
28
|
+
if (!enabled()) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const key = `compose:${id}:${phase}`;
|
|
32
|
+
if (marked.has(key)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
marked.add(key);
|
|
36
|
+
performance.mark(key);
|
|
37
|
+
const start = `compose:${id}:click`;
|
|
38
|
+
if (phase !== "click" && marked.has(start)) {
|
|
39
|
+
performance.measure(`compose:click->${phase}`, { start, end: key, detail: { id } });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -9,6 +9,19 @@ export interface OriginalMessage {
|
|
|
9
9
|
* be. Only asked for by Reply All (see `loadOriginalMessage()`). */
|
|
10
10
|
recipients?: Recipient[];
|
|
11
11
|
}
|
|
12
|
+
/** How long a reply waits for the original's body before it goes ahead with the preview - a slow or hung request must not
|
|
13
|
+
* leave the window waiting on it (or, before it opened at once, keep it from opening) indefinitely. */
|
|
14
|
+
export declare const QUOTE_FETCH_TIMEOUT_MS = 10000;
|
|
15
|
+
/** How long a fetched (or prefetched) body is remembered, so hovering Reply and then clicking it costs one request. */
|
|
16
|
+
export declare const QUOTE_CACHE_MS = 30000;
|
|
17
|
+
/**
|
|
18
|
+
* Starts fetching what a Reply/Forward of `message` will quote, without waiting for the click - called when the pointer or
|
|
19
|
+
* keyboard reaches a Reply/Reply All/Forward button - so the request is usually already answered when it is clicked.
|
|
20
|
+
* Nothing to fetch for an encrypted message (`loadOriginalMessage()` never asks the server for one's body).
|
|
21
|
+
*/
|
|
22
|
+
export declare function prefetchOriginalMessage(message: Message): void;
|
|
23
|
+
/** Forgets every remembered body (the sign-out and test seam). */
|
|
24
|
+
export declare function clearOriginalMessageCache(): void;
|
|
12
25
|
/**
|
|
13
26
|
* Loads what a Reply/Reply All/Forward needs from `message`: the full body to quote - the same content
|
|
14
27
|
* `MessageDetailPane` shows, never the truncated `bodyPreview` unless nothing else can be had - and, with
|
|
@@ -6,6 +6,66 @@ import { apiUrl } from "@rapidmx/react-shared/util/api.js";
|
|
|
6
6
|
import { getMessageRawContent } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
7
7
|
import { decodeHeaderText, extractDisplayBody, parseMimeEntity } from "@rapidmx/react-shared/crypto/mime.js";
|
|
8
8
|
import { parseRecipientList } from "./recipients.js";
|
|
9
|
+
/** How long a reply waits for the original's body before it goes ahead with the preview - a slow or hung request must not
|
|
10
|
+
* leave the window waiting on it (or, before it opened at once, keep it from opening) indefinitely. */
|
|
11
|
+
export const QUOTE_FETCH_TIMEOUT_MS = 10000;
|
|
12
|
+
/** How long a fetched (or prefetched) body is remembered, so hovering Reply and then clicking it costs one request. */
|
|
13
|
+
export const QUOTE_CACHE_MS = 30000;
|
|
14
|
+
const contentCache = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* The server's sanitized HTML body of a message (`GET /mail/messages/:id/content`), or `undefined` when it has none (a
|
|
17
|
+
* `text/plain` message is answered with its preview instead) or the request failed or timed out. Never rejects. Remembered
|
|
18
|
+
* for `QUOTE_CACHE_MS` - except a failure, which is asked again the next time.
|
|
19
|
+
*/
|
|
20
|
+
function fetchContentHtml(uid) {
|
|
21
|
+
const cached = contentCache.get(uid);
|
|
22
|
+
if (cached && Date.now() - cached.at < QUOTE_CACHE_MS) {
|
|
23
|
+
return cached.promise;
|
|
24
|
+
}
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timer = setTimeout(() => controller.abort(), QUOTE_FETCH_TIMEOUT_MS);
|
|
27
|
+
const promise = (async () => {
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch(apiUrl(`/mail/messages/${encodeURIComponent(uid)}/content`), { credentials: "include", signal: controller.signal });
|
|
30
|
+
return res.ok && (res.headers.get("content-type") ?? "").includes("text/html") ? await res.text() : undefined;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
contentCache.set(uid, { at: Date.now(), promise });
|
|
40
|
+
void promise.then((html) => {
|
|
41
|
+
if (html === undefined && contentCache.get(uid)?.promise === promise) {
|
|
42
|
+
contentCache.delete(uid);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return promise;
|
|
46
|
+
}
|
|
47
|
+
/** Rejects when `promise` hasn't settled within `ms` (`getMessageRawContent()` takes no `AbortSignal`, so it is raced rather than aborted). */
|
|
48
|
+
function withTimeout(promise, ms) {
|
|
49
|
+
let timer;
|
|
50
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
51
|
+
timer = setTimeout(() => reject(new Error("Timed out.")), ms);
|
|
52
|
+
});
|
|
53
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Starts fetching what a Reply/Forward of `message` will quote, without waiting for the click - called when the pointer or
|
|
57
|
+
* keyboard reaches a Reply/Reply All/Forward button - so the request is usually already answered when it is clicked.
|
|
58
|
+
* Nothing to fetch for an encrypted message (`loadOriginalMessage()` never asks the server for one's body).
|
|
59
|
+
*/
|
|
60
|
+
export function prefetchOriginalMessage(message) {
|
|
61
|
+
if (!message.encrypted) {
|
|
62
|
+
void fetchContentHtml(message.uid);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Forgets every remembered body (the sign-out and test seam). */
|
|
66
|
+
export function clearOriginalMessageCache() {
|
|
67
|
+
contentCache.clear();
|
|
68
|
+
}
|
|
9
69
|
/** The recipients one address-list header names, RFC 2047-decoded, with their display names. */
|
|
10
70
|
function headerRecipients(value, type) {
|
|
11
71
|
if (!value) {
|
|
@@ -50,23 +110,19 @@ export async function loadOriginalMessage(message, security, options = {}) {
|
|
|
50
110
|
}
|
|
51
111
|
let needsRaw = !!options.recipients && result.recipients === undefined;
|
|
52
112
|
if (!recovered) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
needsRaw = true;
|
|
60
|
-
}
|
|
113
|
+
// Never rejects; nothing (a plain-text message, a failure, a timeout) leaves the raw message below as the other way
|
|
114
|
+
// to the body.
|
|
115
|
+
const html = await fetchContentHtml(message.uid);
|
|
116
|
+
if (html !== undefined) {
|
|
117
|
+
result.body = { html };
|
|
61
118
|
}
|
|
62
|
-
|
|
63
|
-
// The raw message below is the other way to the body.
|
|
119
|
+
else {
|
|
64
120
|
needsRaw = true;
|
|
65
121
|
}
|
|
66
122
|
}
|
|
67
123
|
try {
|
|
68
124
|
if (needsRaw) {
|
|
69
|
-
const entity = parseMimeEntity(await getMessageRawContent(message.uid));
|
|
125
|
+
const entity = parseMimeEntity(await withTimeout(getMessageRawContent(message.uid), QUOTE_FETCH_TIMEOUT_MS));
|
|
70
126
|
if (!recovered && result.body.html === undefined) {
|
|
71
127
|
const body = extractDisplayBody(entity);
|
|
72
128
|
result.body = body.html !== undefined ? { html: body.html } : body.text !== undefined ? { text: body.text } : {};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import React, { PropsWithChildren } from "react";
|
|
2
|
-
import { Folder, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
2
|
+
import { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
3
3
|
import { AppShellProps } from "../../layout/AppShell.js";
|
|
4
|
+
import { LiveUpdates } from "../../../mail/useMailLiveUpdates.js";
|
|
5
|
+
import { MAILBOX_LIST_LIMIT } from "../../../mail/useMailConnection.js";
|
|
6
|
+
import { CountTracker } from "../../../mail/folderCounts.js";
|
|
4
7
|
export type MailShellProps = Omit<AppShellProps, "active">;
|
|
5
8
|
/** One mailbox's own mail folders (already filtered to `MAIL_FOLDER_TYPES` and unsorted) - one entry per
|
|
6
9
|
* mailbox in `mailboxes`, fetched in parallel so every accessible mailbox's folder tree can render
|
|
@@ -41,11 +44,23 @@ export interface MailShellContextValue {
|
|
|
41
44
|
* A no-op on the default context value, which is only ever read outside a real shell.
|
|
42
45
|
*/
|
|
43
46
|
onFolderCreated: (folder: Folder) => void;
|
|
47
|
+
/**
|
|
48
|
+
* Bumped whenever new mail (or another change to a message) may have arrived - a push event, a reconnect or the safety-net
|
|
49
|
+
* poll - so the list on screen can quietly refetch its first page. See `useMailLiveUpdates()`. Never changes on the default
|
|
50
|
+
* context value, which is only ever read outside a real shell.
|
|
51
|
+
*/
|
|
52
|
+
live: LiveUpdates;
|
|
53
|
+
/**
|
|
54
|
+
* Applies a change to a message (`previous` to `next`; `next` absent when it was deleted) to the folder badges at once, and
|
|
55
|
+
* hands back what to call when the server has answered: `settle()` keeps it, `revert()` takes it back. Every place that
|
|
56
|
+
* reads, moves or deletes a message goes through this (see `setReadState()`), so the badges follow. A no-op on the default
|
|
57
|
+
* context value, which is only ever read outside a real shell.
|
|
58
|
+
*/
|
|
59
|
+
trackMessageChange: (previous: Message, next: Message | null) => CountTracker;
|
|
44
60
|
}
|
|
45
61
|
/** Reads the mailbox/folder a page is currently showing, as resolved by the enclosing `MailShell`. */
|
|
46
62
|
export declare function useMailShell(): MailShellContextValue;
|
|
47
|
-
|
|
48
|
-
export declare const MAILBOX_LIST_LIMIT = 100;
|
|
63
|
+
export { MAILBOX_LIST_LIMIT };
|
|
49
64
|
/**
|
|
50
65
|
* Mail's own contextual sidebar (every accessible mailbox's own folder tree, plus a merged "All Mailboxes"
|
|
51
66
|
* aggregate section) + content area, rendered inside the shared `AppShell` chrome (icon rail, header,
|
|
@@ -62,4 +77,4 @@ export declare const MAILBOX_LIST_LIMIT = 100;
|
|
|
62
77
|
* aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
|
|
63
78
|
* of folders" requirement.
|
|
64
79
|
*/
|
|
65
|
-
export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<MailShellProps>): React.JSX.Element;
|
|
80
|
+
export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }: PropsWithChildren<MailShellProps>): React.JSX.Element;
|