@rapidmx/web-client 0.5.0 → 0.6.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 (164) hide show
  1. package/README.md +88 -0
  2. package/apps/shared/components/admin/layout/AdminShell.tsx +25 -8
  3. package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
  4. package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
  5. package/apps/shared/components/layout/AppShell.tsx +36 -18
  6. package/apps/shared/components/mail/layout/MailShell.tsx +420 -418
  7. package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -9
  8. package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
  9. package/apps/shared/plugins/pluginNav.ts +73 -0
  10. package/apps/shared/search/localIndexRpcClient.ts +3 -1
  11. package/dist/apps/admin/_404.d.ts +2 -0
  12. package/dist/apps/admin/_500.d.ts +2 -0
  13. package/dist/apps/admin/_layout.d.ts +8 -0
  14. package/dist/apps/admin/audit-log/index.d.ts +11 -0
  15. package/dist/apps/admin/branding/index.d.ts +3 -0
  16. package/dist/apps/admin/data-requests/index.d.ts +3 -0
  17. package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
  18. package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
  19. package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
  20. package/dist/apps/admin/domains/[uid].d.ts +7 -0
  21. package/dist/apps/admin/domains/index.d.ts +3 -0
  22. package/dist/apps/admin/domains/new/index.d.ts +3 -0
  23. package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
  24. package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
  25. package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
  26. package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
  27. package/dist/apps/admin/index.d.ts +3 -0
  28. package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
  29. package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
  30. package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
  31. package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
  32. package/dist/apps/admin/plugins/index.d.ts +3 -0
  33. package/dist/apps/admin/quarantine/index.d.ts +6 -0
  34. package/dist/apps/admin/retention-policy/index.d.ts +3 -0
  35. package/dist/apps/admin/setup/index.d.ts +3 -0
  36. package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
  37. package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
  38. package/dist/apps/admin/transport-rules/index.d.ts +3 -0
  39. package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
  40. package/dist/apps/escrow/_layout.d.ts +10 -0
  41. package/dist/apps/escrow/audit-log/index.d.ts +9 -0
  42. package/dist/apps/escrow/index.d.ts +3 -0
  43. package/dist/apps/escrow/matters/[uid].d.ts +7 -0
  44. package/dist/apps/escrow/matters/new/index.d.ts +3 -0
  45. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
  46. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
  47. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
  48. package/dist/apps/shared/components/admin/layout/AdminShell.js +12 -5
  49. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
  50. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
  51. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
  52. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
  53. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +9 -0
  54. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
  55. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +12 -0
  56. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
  57. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
  58. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +10 -0
  59. package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +4 -0
  60. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +9 -0
  61. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
  62. package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
  63. package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
  64. package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
  65. package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
  66. package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
  67. package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
  68. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
  69. package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
  70. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
  71. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
  72. package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
  73. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
  74. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +2 -2
  75. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
  76. package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
  77. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
  78. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
  79. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
  80. package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
  81. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
  82. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
  83. package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
  84. package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
  85. package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
  86. package/dist/apps/shared/components/layout/AppShell.js +15 -13
  87. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
  88. package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
  89. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
  90. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
  91. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
  92. package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
  93. package/dist/apps/shared/components/mail/ConversationList.d.ts +11 -0
  94. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +23 -0
  95. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +93 -0
  96. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +58 -0
  97. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
  98. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
  99. package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
  100. package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
  101. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +34 -0
  102. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
  103. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
  104. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +57 -0
  105. package/dist/apps/shared/components/mail/layout/MailShell.js +2 -2
  106. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
  107. package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
  108. package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
  109. package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
  110. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
  111. package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -9
  112. package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
  113. package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
  114. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
  115. package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
  116. package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
  117. package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
  118. package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
  119. package/dist/apps/shared/plugins/pluginNav.js +33 -0
  120. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
  121. package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
  122. package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
  123. package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
  124. package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
  125. package/dist/apps/shared/search/localIndexRpcClient.js +3 -1
  126. package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
  127. package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
  128. package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
  129. package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
  130. package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
  131. package/dist/apps/shared/search/searchTier2.d.ts +30 -0
  132. package/dist/apps/www/_404.d.ts +2 -0
  133. package/dist/apps/www/_500.d.ts +2 -0
  134. package/dist/apps/www/_layout.d.ts +14 -0
  135. package/dist/apps/www/calendar/index.d.ts +3 -0
  136. package/dist/apps/www/contacts/[uid].d.ts +13 -0
  137. package/dist/apps/www/contacts/index.d.ts +3 -0
  138. package/dist/apps/www/index.d.ts +3 -0
  139. package/dist/apps/www/messages/[uid].d.ts +11 -0
  140. package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
  141. package/dist/apps/www/settings/encryption/index.d.ts +7 -0
  142. package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
  143. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
  144. package/dist/apps/www/settings/filters/index.d.ts +4 -0
  145. package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
  146. package/dist/apps/www/settings/focused-inbox/index.d.ts +4 -0
  147. package/dist/apps/www/settings/labels/index.d.ts +4 -0
  148. package/dist/apps/www/settings/privacy/index.d.ts +4 -0
  149. package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
  150. package/dist/apps/www/settings/sharing/index.d.ts +4 -0
  151. package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
  152. package/dist/apps/www/settings/signatures/index.d.ts +4 -0
  153. package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
  154. package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
  155. package/dist/apps/www/tasks/index.d.ts +3 -0
  156. package/package.json +3 -3
  157. package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
  158. package/apps/www/settings/booking-types/[uid].tsx +0 -306
  159. package/apps/www/settings/booking-types/index.tsx +0 -98
  160. package/apps/www/settings/booking-types/new/index.tsx +0 -212
  161. package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
  162. package/dist/apps/www/settings/booking-types/[uid].js +0 -134
  163. package/dist/apps/www/settings/booking-types/index.js +0 -29
  164. package/dist/apps/www/settings/booking-types/new/index.js +0 -86
@@ -0,0 +1,49 @@
1
+ import "../../styles/app.css";
2
+ import React, { PropsWithChildren } from "react";
3
+ import type { IconType } from "react-icons";
4
+ import { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
5
+ import { PluginNav, PluginNavProps } from "../../plugins/pluginNav.js";
6
+ /** How long sign-out waits for auth-server's logout before navigating anyway. */
7
+ export declare const LOGOUT_TIMEOUT_MS = 3000;
8
+ export type AppShellApp = "mail" | "calendar" | "contacts" | "tasks";
9
+ /** `"settings"` is a valid `active` value but deliberately has no entry in `APPS` below — Settings is
10
+ * reached via a `UserMenu` item, not a 5th rail icon (see `SettingsShell.tsx`), so it highlights no
11
+ * rail/tab icon at all; only the header title needs to account for it. Any other string is a plugin's
12
+ * `appRail` item id (see `PluginNav`). */
13
+ export type AppShellActive = AppShellApp | "settings" | (string & {});
14
+ export interface AppShellProps extends PluginNavProps {
15
+ /** Which icon in the rail is highlighted as the current app — `"settings"` highlights none, and a plugin
16
+ * app page passes its own `appRail` item id. */
17
+ active: AppShellActive;
18
+ /** Populated automatically by the framework from an authenticated request (e.g. a valid `jwt` cookie). */
19
+ userUid?: string;
20
+ /** auth-server's base URL, injected via the route's `fetchProps`. */
21
+ authServerUrl?: string;
22
+ /**
23
+ * `true` when this session is an admin "log in as user" impersonation (a `jwt_impersonator` cookie is
24
+ * present) — see `MailShell`'s original doc comment, unchanged now that this lives here. Drives the
25
+ * "you are viewing as this user — stop impersonating" banner below.
26
+ */
27
+ impersonating?: boolean;
28
+ /** Where `mailApi.ts`'s `stopImpersonating()` should call — see `MailShell`'s original doc comment. */
29
+ impersonationBaseUrl?: string;
30
+ /** `true` when the caller's JWT carries a trusted role — shows an "Admin" item in the user menu below. */
31
+ trusted?: boolean;
32
+ }
33
+ export interface AppDef {
34
+ id: AppShellApp;
35
+ href: string;
36
+ label: string;
37
+ icon: IconType;
38
+ }
39
+ export declare const APPS: AppDef[];
40
+ /** `APPS` followed by the plugins' `appRail` items (generic icon), core ids winning - see `mergePluginNavItems`. */
41
+ export declare function appRailItems(pluginNav?: PluginNav): NavItem[];
42
+ /**
43
+ * The persistent chrome shared by every webmail app (Mail, Calendar, Contacts, Tasks): a left icon rail for
44
+ * switching apps (full page loads — see `MailShell`'s original doc comment on this framework having no
45
+ * client-side router), a header with the current app's name and `UserMenu`, and the impersonation banner.
46
+ * Each app's own shell (e.g. `MailShell`) renders its own contextual sidebar + content as `children`, inside
47
+ * the area to the right of the icon rail and below the header.
48
+ */
49
+ export default function AppShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<AppShellProps>): React.JSX.Element;
@@ -5,7 +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, useRef, useState } from "react";
8
- import { HiOutlineCalendarDays, HiOutlineClipboardDocumentList, HiOutlineEnvelope, HiOutlineUsers } from "react-icons/hi2";
8
+ import { HiOutlineCalendarDays, HiOutlineClipboardDocumentList, HiOutlineEnvelope, HiOutlinePuzzlePiece, HiOutlineUsers, } from "react-icons/hi2";
9
9
  import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
10
10
  import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
11
11
  import { stopImpersonating } from "@rapidmx/react-shared/mail/mailApi.js";
@@ -21,6 +21,7 @@ import { SIGN_OUT_CHANNEL, destroyAllLocalIndexes } from "../../search/localInde
21
21
  import { authApiFetch } from "@rapidmx/react-shared/util/api.js";
22
22
  import { destroyUnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
23
23
  import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
24
+ import { mergePluginNavItems } from "../../plugins/pluginNav.js";
24
25
  /** How long sign-out waits for auth-server's logout before navigating anyway. */
25
26
  export const LOGOUT_TIMEOUT_MS = 3000;
26
27
  /**
@@ -50,15 +51,13 @@ export const APPS = [
50
51
  { id: "contacts", href: "/contacts", label: "Contacts", icon: HiOutlineUsers },
51
52
  { id: "tasks", href: "/tasks", label: "Tasks", icon: HiOutlineClipboardDocumentList },
52
53
  ];
53
- /** The header title for every valid `active` value — a superset of `APPS`' own labels since `"settings"`
54
- * has no rail icon (and so no `AppDef`) but still needs a header title. */
55
- const ACTIVE_LABELS = {
56
- mail: "Mail",
57
- calendar: "Calendar",
58
- contacts: "Contacts",
59
- tasks: "Tasks",
60
- settings: "Settings",
61
- };
54
+ /** Ids plugin `appRail` items can't take besides `APPS`' own - `"settings"` has no rail icon but is still a
55
+ * core `active` value. */
56
+ const RESERVED_APP_IDS = ["settings"];
57
+ /** `APPS` followed by the plugins' `appRail` items (generic icon), core ids winning - see `mergePluginNavItems`. */
58
+ export function appRailItems(pluginNav) {
59
+ return mergePluginNavItems(APPS, pluginNav?.appRail, ({ id, href, label }) => ({ id, href, label, icon: HiOutlinePuzzlePiece }), RESERVED_APP_IDS);
60
+ }
62
61
  /**
63
62
  * The persistent chrome shared by every webmail app (Mail, Calendar, Contacts, Tasks): a left icon rail for
64
63
  * switching apps (full page loads — see `MailShell`'s original doc comment on this framework having no
@@ -66,7 +65,7 @@ const ACTIVE_LABELS = {
66
65
  * Each app's own shell (e.g. `MailShell`) renders its own contextual sidebar + content as `children`, inside
67
66
  * the area to the right of the icon rail and below the header.
68
67
  */
69
- export default function AppShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, children, }) {
68
+ export default function AppShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
70
69
  const [stoppingImpersonation, setStoppingImpersonation] = useState(false);
71
70
  const signingOutRef = useRef(false);
72
71
  const { branding, iconSrc } = useBranding();
@@ -155,10 +154,13 @@ export default function AppShell({ active, userUid, authServerUrl, impersonating
155
154
  if (!userUid) {
156
155
  return _jsx("div", { className: "min-h-screen" });
157
156
  }
158
- return (_jsx(UnlockPromptProvider, { children: _jsx(ComposeProvider, { userUid: userUid, trusted: !!trusted && !impersonating, children: _jsxs("div", { className: "min-h-screen flex flex-col bg-surface-alt", children: [_jsx(BrandingHeader, { branding: branding }), impersonating && (_jsxs("div", { className: "h-10 shrink-0 bg-warning text-warning-contrast flex items-center justify-center gap-3 text-sm font-medium px-4", children: [_jsxs("span", { children: ["You are viewing as ", _jsx("strong", { children: userUid }), "."] }), _jsx("button", { type: "button", onClick: handleStopImpersonating, disabled: stoppingImpersonation, className: "underline hover:no-underline disabled:opacity-60", children: stoppingImpersonation ? "Returning to admin…" : "Return to admin" })] })), _jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsxs("nav", { "aria-label": "Apps", className: "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1", children: [_jsx("img", { src: iconSrc, width: "96", height: "96", alt: "", className: "mb-3" }), APPS.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
157
+ const apps = appRailItems(pluginNav);
158
+ // The header title: "settings" has no rail item, everything else is labelled by its own rail item.
159
+ const activeLabel = active === "settings" ? "Settings" : apps.find((app) => app.id === active)?.label;
160
+ return (_jsx(UnlockPromptProvider, { children: _jsx(ComposeProvider, { userUid: userUid, trusted: !!trusted && !impersonating, children: _jsxs("div", { className: "min-h-screen flex flex-col bg-surface-alt", children: [_jsx(BrandingHeader, { branding: branding }), impersonating && (_jsxs("div", { className: "h-10 shrink-0 bg-warning text-warning-contrast flex items-center justify-center gap-3 text-sm font-medium px-4", children: [_jsxs("span", { children: ["You are viewing as ", _jsx("strong", { children: userUid }), "."] }), _jsx("button", { type: "button", onClick: handleStopImpersonating, disabled: stoppingImpersonation, className: "underline hover:no-underline disabled:opacity-60", children: stoppingImpersonation ? "Returning to admin…" : "Return to admin" })] })), _jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsxs("nav", { "aria-label": "Apps", className: "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1", children: [_jsx("img", { src: iconSrc, width: "96", height: "96", alt: "", className: "mb-3" }), apps.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
159
161
  "w-10 h-10 flex items-center justify-center rounded-sm",
160
162
  id === active
161
163
  ? "bg-primary/10 text-primary-dark"
162
164
  : "text-text-muted hover:bg-surface-alt hover:text-text",
163
- ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: APPS, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "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: ACTIVE_LABELS[active] }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut, showAdminLink: trusted, showSettingsLink: true })] }), _jsx("div", { className: "flex-1 flex min-h-0 pb-14 md:pb-0", children: children })] })] }), _jsx(BrandingFooter, { branding: branding })] }) }) }));
165
+ ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: apps, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "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 }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut, showAdminLink: trusted, showSettingsLink: true })] }), _jsx("div", { className: "flex-1 flex min-h-0 pb-14 md:pb-0", children: children })] })] }), _jsx(BrandingFooter, { branding: branding })] }) }) }));
164
166
  }
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
3
+ /**
4
+ * Sanitizes admin-configured branding HTML before it's rendered. The server sanitizes on save too; this also
5
+ * covers anything stored before that. Returns `""` where there's no DOM to sanitize with (server-side
6
+ * rendering), so unsanitized HTML is never emitted.
7
+ */
8
+ export declare function sanitizeBrandingHtml(html: string): string;
9
+ /**
10
+ * Renders the admin-configured `Branding.headerHtml`/`footerHtml` (see `useBranding()`), sanitized - shared by
11
+ * the webmail and admin chromes. The escrow console deliberately renders no branding HTML at all.
12
+ */
13
+ export declare function BrandingHeader({ branding }: {
14
+ branding: Branding | null;
15
+ }): React.JSX.Element;
16
+ export declare function BrandingFooter({ branding }: {
17
+ branding: Branding | null;
18
+ }): React.JSX.Element;
@@ -0,0 +1,51 @@
1
+ import React from "react";
2
+ import { PublicKey } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
3
+ export interface KeyEnrollmentGateProps {
4
+ /** Undefined while the caller's mailbox hasn't resolved yet (or has none) - renders `children`
5
+ * unchanged in that case, so this component can be mounted unconditionally at a stable tree
6
+ * position (see this component's own doc comment for why that matters). */
7
+ mailboxUid?: string;
8
+ mailboxAddress?: string;
9
+ /** The mailbox's currently-published public keys (`Mailbox.keys`) - needed to unlock an
10
+ * already-enrolled vault (see `unlockWithPassword()`'s own signature), not just to provision a new
11
+ * one. Treated as empty when undefined - a mailbox with no public keys published yet has nothing to
12
+ * unlock, so this only affects the already-enrolled path. */
13
+ mailboxKeys?: PublicKey[];
14
+ /**
15
+ * `true` (default) preserves this component's original behavior: a mailbox with an existing vault
16
+ * but no unlocked session blocks `children` behind a full-page unlock form. Set to `false` for a
17
+ * mount point that shouldn't block merely because a mailbox resolved - `MailShell` does this, since
18
+ * unlocking is only actually required to sign/encrypt a compose, read an already-encrypted message,
19
+ * or change encryption settings (see `UnlockPromptProvider.tsx`'s `useUnlockPrompt()`, which those
20
+ * specific call sites use instead). Has no effect on first-time provisioning
21
+ * (`setup_password`/`enrolling`/`show_recovery_codes`), which still always blocks regardless - that
22
+ * only ever happens once per mailbox and is a genuine prerequisite, not the source of "unlock keeps
23
+ * popping up" friction this prop exists to avoid.
24
+ */
25
+ blocking?: boolean;
26
+ /**
27
+ * `true` when the caller may provision this mailbox's first encryption key - i.e. they own it and aren't
28
+ * an administrator impersonating them. `false` (default, so a caller that forgets it never provisions) for
29
+ * a shared/delegated mailbox the caller doesn't own, or an impersonated session: a mailbox with no vault yet then
30
+ * renders `children` instead of the "Protect your mailbox" setup, so a delegate never enrolls keys
31
+ * (and recovery codes) for someone else's mailbox. Unlocking an existing vault is unaffected.
32
+ */
33
+ canProvision?: boolean;
34
+ children: React.ReactNode;
35
+ }
36
+ /**
37
+ * Gates a mailbox's normal content behind one-time E2E encryption key setup, per
38
+ * `specs/end-to-end_encryption.md`'s "Generate signing and encryption keypairs on the client's device on
39
+ * first sign-in." Rendered by `MailShell` wrapping its own returned content unconditionally, not only
40
+ * once a mailbox is resolved — mounting this component at a *different* tree position depending on
41
+ * `mailboxUid`'s readiness (e.g. only wrapping once ready, rendering bare content beforehand) would make
42
+ * `children` (real `AppShell` chrome) itself remount the moment `mailboxUid` resolves, tearing down any
43
+ * state/effects it had already started. Always wrapping keeps `AppShell` mounted continuously across
44
+ * that transition; this component simply passes `children` through untouched while `mailboxUid` is
45
+ * still unresolved, and only starts its own check once a real `mailboxUid` is supplied.
46
+ *
47
+ * A `getKeyVault()` failure (network error, server not yet wired up to serve this endpoint) also renders
48
+ * `children` unchanged rather than blocking mail entirely - encryption is optional and gradual by design
49
+ * (the spec's own "near zero frictionless experience" goal), not a hard prerequisite for reading mail.
50
+ */
51
+ export default function KeyEnrollmentGate({ mailboxUid, mailboxAddress, mailboxKeys, blocking, canProvision, children, }: KeyEnrollmentGateProps): React.JSX.Element | null;
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ /**
3
+ * Rendered by each app's shell (Mail/Calendar/Contacts/Tasks) *instead of* the normal `AppShell`
4
+ * chrome (icon rail, header, folder tree, ...) when the caller has no mailbox — a full-screen,
5
+ * 404-style takeover, not a small message nested inside the rest of the client UI, since there's
6
+ * nothing else in the app for a mailbox-less caller to do yet. Attempts self-service
7
+ * auto-provisioning — see `BaseMailboxRoute.autoProvision()`'s own doc comment in `@rapidmx/restapi`
8
+ * for the full contract — which is itself a no-op (a 404) unless an admin has both turned it on
9
+ * (`mail:auto_provision:enabled`) and configured at least one domain (`mail:domains`). Safe to render
10
+ * unconditionally in that place: any failure (disabled, no registered username, the identity service
11
+ * unreachable) just falls back to the same plain "no mailbox" message this replaces.
12
+ */
13
+ export default function MailboxProvisioning(): React.JSX.Element;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Unlocking with a recovery code, and what has to happen after it. A recovery code is single-use: once it has opened
3
+ * the keys, the user may set a new encryption password (`replacePasswordWrap()`), and then the used code is removed
4
+ * (`consumeRecoveryCode()`). The order matters - react-shared's `replacePasswordWrap()` refuses unless another own
5
+ * unlock method remains, and the not-yet-consumed code is that method.
6
+ *
7
+ * `UnlockPromptProvider` and `KeyEnrollmentGate` both unlock with `startRecoveryUnlock()`, settle whatever was waiting
8
+ * on the unlock straight away, and then show `RecoveryFollowUpModal` for the follow-up steps.
9
+ */
10
+ import React from "react";
11
+ import { type PublicKey } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
12
+ /** The same minimum every other "set an encryption password" form in this app uses. */
13
+ export declare const MIN_PASSWORD_LENGTH = 8;
14
+ /** Where recovery codes are regenerated. */
15
+ export declare const ENCRYPTION_SETTINGS_HREF = "/settings/encryption";
16
+ /** Deliberately generic: a wrong code and a vault with no recovery codes fail the same way. */
17
+ export declare const RECOVERY_CODE_ERROR = "That recovery code didn't work.";
18
+ export declare const REPLACE_ROTATED_MESSAGE = "Your encryption keys were changed on another device after you unlocked, so your password wasn't changed and your recovery code wasn't used. Reload the page and unlock again.";
19
+ export declare const REPLACE_MULTIPLE_PASSWORDS_MESSAGE = "This mailbox has more than one encryption password, so it can't be replaced here. Nothing was changed. Remove the extra password in Settings > Encryption first.";
20
+ export declare const REPLACE_NO_OTHER_METHOD_MESSAGE = "Your password wasn't changed, because this mailbox would be left with no other way to unlock. Your recovery code may already have been used in another window. Nothing was changed.";
21
+ export declare const REPLACE_ADD_FAILED_RESTORED_MESSAGE = "Your new password couldn't be saved, so your old password still works. Nothing was changed. Try again.";
22
+ export declare const REPLACE_ADD_FAILED_NOT_RESTORED_MESSAGE = "Your new password couldn't be saved, and your old password couldn't be put back, so this mailbox has no password right now. Your recovery code still works. Try again, or keep the code for now.";
23
+ export declare const REPLACE_LOCKED_MESSAGE = "Your encryption keys were locked before your new password could be set, so nothing was changed and your recovery code still works. Unlock with it again to set a new password.";
24
+ export declare const REPLACE_GENERIC_MESSAGE = "Your new password couldn't be saved. Nothing was changed. Try again.";
25
+ export declare const CONSUME_FAILED_MESSAGE = "The recovery code you used couldn't be removed, so it may still work. Regenerate your recovery codes in Settings > Encryption to make sure it can't be used again.";
26
+ /** What a successful recovery-code unlock hands to `RecoveryFollowUpModal`. */
27
+ export interface RecoveryFollowUp {
28
+ mailboxUid: string;
29
+ recoveryMethodId?: string;
30
+ /** The codes left once the used one is consumed. */
31
+ remainingRecoveryCodes: number;
32
+ /** The vault's generation read *before* the unlock - see `startRecoveryUnlock()`. */
33
+ expectedMasterKeyGeneration?: number;
34
+ unopenableKeys: string[];
35
+ }
36
+ /**
37
+ * Unlocks `mailboxUid` with a recovery code (normalized by react-shared, so spacing and case don't matter). The
38
+ * vault's `masterKeyGeneration` is read first: if another device rotates the master key at any point after that read,
39
+ * the generation passed to `replacePasswordWrap()` later is stale and the replacement is refused, never written with
40
+ * a dead master key. A failed read just means no generation check. Rejects exactly as `unlockWithRecoveryCode()` does.
41
+ */
42
+ export declare function startRecoveryUnlock(mailboxUid: string, mailboxKeys: PublicKey[], code: string): Promise<RecoveryFollowUp>;
43
+ /** A "use a recovery code instead" / "use your password instead" switch for an unlock form. */
44
+ export declare function UnlockModeToggle({ mode, disabled, onChange, }: {
45
+ mode: "password" | "recovery";
46
+ disabled: boolean;
47
+ onChange: (mode: "password" | "recovery") => void;
48
+ }): React.JSX.Element;
49
+ export interface RecoveryFollowUpModalProps {
50
+ followUp: RecoveryFollowUp;
51
+ /** `false` hides the dialog without losing its progress (e.g. while another unlock dialog is on top). */
52
+ open?: boolean;
53
+ onDone: () => void;
54
+ }
55
+ /**
56
+ * The steps after a recovery-code unlock, shown once the keys are already open:
57
+ *
58
+ * 1. Offer a new encryption password. Optional ("Skip") - unless this was the last code, where it's required before the
59
+ * code is used up (otherwise only a forgotten password would remain); then "Keep this code for now" closes without
60
+ * consuming anything.
61
+ * 2. Remove the used code. A failure is reported but never blocks anything - the keys are already open.
62
+ * 3. Say how many codes are left, nudging towards regenerating them at two or fewer.
63
+ *
64
+ * Mount it with a `key` per follow-up so each unlock starts from step 1.
65
+ */
66
+ export declare function RecoveryFollowUpModal({ followUp, open, onDone }: RecoveryFollowUpModalProps): React.JSX.Element;
@@ -0,0 +1,45 @@
1
+ import React from "react";
2
+ import { UnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
3
+ import type { PublicKey } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
4
+ /** Shown instead of "Incorrect password" when the password was right but the mailbox's encryption key itself
5
+ * won't open (`UnopenableEncryptionKeyError`) - retrying can't fix that, so it must not read as a typo. */
6
+ export declare const UNOPENABLE_ENCRYPTION_KEY_MESSAGE = "Your password is correct, but one of your keys couldn't be opened, so encrypted mail can't be read or sent from this mailbox. Trying again won't help - contact support.";
7
+ /** `UNOPENABLE_ENCRYPTION_KEY_MESSAGE`'s counterpart for a recovery-code unlock. */
8
+ export declare const UNOPENABLE_ENCRYPTION_KEY_RECOVERY_MESSAGE = "Your recovery code is correct, but one of your keys couldn't be opened, so encrypted mail can't be read or sent from this mailbox. Trying again won't help - contact support.";
9
+ /** The error text for a failed `unlockWithPassword()` (or, with `method` `"recovery"`, `unlockWithRecoveryCode()`). */
10
+ export declare function unlockErrorMessage(err: unknown, method?: "password" | "recovery"): string;
11
+ /**
12
+ * A non-blocking notice after an unlock that succeeded without some signing keys (`UnlockResult.unopenableKeys`):
13
+ * mail can still be read, but not signed with those keys on this device.
14
+ */
15
+ export declare function UnopenableKeysNotice({ fingerprints, onDismiss }: {
16
+ fingerprints: string[];
17
+ onDismiss: () => void;
18
+ }): React.JSX.Element;
19
+ interface UnlockPromptContextValue {
20
+ /**
21
+ * Resolves immediately with this mailbox's already-unlocked keys if it has any this session -
22
+ * showing no UI at all. Otherwise shows an unlock dialog on top of whatever the user is currently
23
+ * doing and resolves once they enter the correct password, or rejects if they dismiss it.
24
+ */
25
+ requestUnlock(mailboxUid: string, mailboxKeys: PublicKey[]): Promise<UnlockedKeys>;
26
+ }
27
+ /**
28
+ * The on-demand counterpart to `KeyEnrollmentGate`'s blocking, first-sign-in-only provisioning flow.
29
+ * Per `specs/end-to-end_encryption.md`, unlocking is only actually *required* to (1) sign or encrypt an
30
+ * outgoing message, (2) read an already-encrypted message, or (3) change encryption settings that touch
31
+ * the master key - not merely to have a mailbox with an existing vault resolve. Mounted once in
32
+ * `AppShell.tsx` (shared by every top-level app shell: Mail, Calendar, Contacts, Tasks, Settings), so
33
+ * any of those three call sites (`ComposeWindow`'s sign/encrypt toggles, `MessageDetailPane`'s encrypted-
34
+ * message view, `Settings > Encryption`) can request an unlock right where it's needed via
35
+ * `useUnlockPrompt()`, instead of the whole app being blocked behind an unlock screen the moment a
36
+ * mailbox happens to resolve - see `KeyEnrollmentGate`'s `blocking` prop, which `MailShell` now sets to
37
+ * `false` for exactly this reason.
38
+ */
39
+ export declare function UnlockPromptProvider({ children }: {
40
+ children: React.ReactNode;
41
+ }): React.JSX.Element;
42
+ /** Throws outside an `<UnlockPromptProvider>` - every app shell mounts one (see `AppShell.tsx`), so
43
+ * reaching this from any real page is a wiring bug, not a runtime condition to degrade gracefully from. */
44
+ export declare function useUnlockPrompt(): UnlockPromptContextValue;
45
+ export {};
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ export interface UserMenuProps {
3
+ userUid: string;
4
+ /** auth-server's base URL — where the caller's own profile (name/avatar) is fetched from, if configured. */
5
+ authServerUrl?: string;
6
+ onSignOut: () => void;
7
+ /** Shows an "Admin" item linking to `/admin`, above "Sign Out" — pass only for a trusted-role caller. */
8
+ showAdminLink?: boolean;
9
+ /** Shows a "Settings" item, above "Admin"/"Sign Out" — an account-scoped surface (like "Admin"), not
10
+ * a content-scoped one, so it lives here rather than as a 5th `AppShell` rail icon. Links straight to
11
+ * `/settings/auto-reply`, the only settings section that exists today; repoint this at a real
12
+ * `/settings` landing page once a second section (Mail Filters, Signatures) makes one worth building. */
13
+ showSettingsLink?: boolean;
14
+ }
15
+ /**
16
+ * The top-right "who am I" menu shared by `MailShell` and `AdminShell`: an avatar-button trigger that opens a
17
+ * dropdown showing the caller's avatar/name, an optional "Admin" link (trusted-role callers only), and
18
+ * "Sign Out". Name/avatar come from auth-server's own profile endpoint (`GET /api/profiles/me`) — this
19
+ * service has no local user directory (see `.claude/NOTES.md`) — and fall back to the bare uid/its first
20
+ * letter when unset or unreachable, which is a fully valid, expected state for most password-registered
21
+ * accounts (only OIDC sign-in currently populates a real avatar).
22
+ */
23
+ export default function UserMenu({ userUid, authServerUrl, onSignOut, showAdminLink, showSettingsLink }: UserMenuProps): React.JSX.Element;
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import { ConversationSummary } from "@rapidmx/react-shared/mail/conversationsApi.js";
3
+ export interface ConversationListProps {
4
+ conversations: ConversationSummary[];
5
+ selectedId: string | null;
6
+ onSelect: (conversation: ConversationSummary) => void;
7
+ }
8
+ /** The "By conversation" counterpart to the per-message list `apps/www/index.tsx` renders inline for
9
+ * "By date" — conversations are mailbox-wide (see `conversationsApi.ts`), so there's no per-folder
10
+ * empty/loading state to distinguish here beyond what the page itself already handles. */
11
+ export default function ConversationList({ conversations, selectedId, onSelect }: ConversationListProps): React.JSX.Element;
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { Folder } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ import { ConversationSummary } from "@rapidmx/react-shared/mail/conversationsApi.js";
4
+ import { Label } from "@rapidmx/react-shared/mail/labelsApi.js";
5
+ export interface ConversationThreadPaneProps {
6
+ conversation: ConversationSummary | null;
7
+ /** The mailbox's full folder list — a conversation can span folders (e.g. an Inbox message and the
8
+ * Sent Items copy of its reply), so each message's own recall eligibility is looked up individually
9
+ * against its own `folderUid` rather than assuming one folder for the whole thread. */
10
+ folders: Folder[];
11
+ /** The mailbox's labels, fetched once by the parent (`apps/www/index.tsx`) and passed through to
12
+ * every expanded message's own `MessageDetailPane` — see that component's own `labels` prop. */
13
+ labels?: Label[];
14
+ }
15
+ /**
16
+ * The merged, Gmail-style reading pane for "By conversation" mode — every message in the conversation
17
+ * fetched and stacked oldest-to-newest, all but the most recent collapsed to a one-line summary
18
+ * (sender + subject) until clicked. Reuses `MessageDetailPane` for each *expanded* message's own
19
+ * header/attachments/body-iframe rendering rather than re-implementing it; a collapsed row is its own
20
+ * lightweight summary, since mounting a full `MessageDetailPane` (and its iframe) for every message in
21
+ * a long thread up front would be wasteful.
22
+ */
23
+ export default function ConversationThreadPane({ conversation, folders, labels }: ConversationThreadPaneProps): React.JSX.Element;
@@ -0,0 +1,93 @@
1
+ import React from "react";
2
+ import { Attachment, Message } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ import { Label } from "@rapidmx/react-shared/mail/labelsApi.js";
4
+ import { MessageSecurityResult } from "@rapidmx/react-shared/crypto/messageSecurity.js";
5
+ /** The detail line under a `verified_at_first_open` badge. */
6
+ export declare function verifiedAtFirstOpenMessage(result: MessageSecurityResult): string;
7
+ export interface SenderNameCheck {
8
+ /** The display name contains an `@` or a look-alike - it reads as (part of) an address. */
9
+ looksLikeAddress: boolean;
10
+ /** It shows an address-shaped token that isn't `actualAddress` - e.g. `"ceo@corp.com" <x@corp-pay.com>`. */
11
+ misleading: boolean;
12
+ }
13
+ /** Whether a sender's display name poses as an address, and whether that address differs from the one actually sent
14
+ * from. Compatibility forms are folded first (NFKC turns the fullwidth and small @ into `@`), so a look-alike can't
15
+ * hide the token. */
16
+ export declare function checkSenderName(displayName: string | undefined, actualAddress: string): SenderNameCheck;
17
+ export declare const TRUST_SIGNER_CONFLICT_MESSAGE = "A different signing key is already trusted for this sender, so this one wasn't trusted. The sender's key may have changed; confirm with them before doing anything.";
18
+ export declare const TRUST_SIGNER_INVALID_MESSAGE = "This certificate can't be trusted for this sender.";
19
+ export declare const TRUST_SIGNER_FORBIDDEN_MESSAGE = "You don't have permission to trust signers for this mailbox.";
20
+ export declare const TRUST_SIGNER_GENERIC_MESSAGE = "Couldn't trust this signer. Try again.";
21
+ export declare const KEPT_CURRENT_SIGNING_KEY_MESSAGE = "You kept the current signing key for this sender. This message stays unverified.";
22
+ /** The error text for a failed `trustSigner()`. */
23
+ export declare function trustSignerErrorMessage(err: unknown): string;
24
+ /** Groups a fingerprint into 4-character blocks (`AB12 CD34 ...`), ignoring any separators it came with, so it can be
25
+ * read out and compared with the sender over another channel. */
26
+ export declare function formatFingerprint(fingerprint: string): string;
27
+ /** Decodes CSS escapes first (so an escaped `u\72l(` can't hide from the checks below), then drops every
28
+ * `@import` and neutralizes every `url()`/`image-set()` reference that isn't a `data:`/`cid:` URI. */
29
+ export declare function stripRemoteCssUrls(css: string): string;
30
+ /** Sanitizes a client-rendered body for `srcDoc`, with the CSP meta as its very first element. */
31
+ export declare function buildSecureSrcDoc(html: string): string;
32
+ export interface MessageDetailPaneProps {
33
+ message: Message | null;
34
+ attachments: Attachment[];
35
+ /** Present only on the mobile detail route — renders a "back to messages" link above the header. Absent
36
+ * on the desktop reading pane, which never navigates away (selecting a different message just swaps
37
+ * `message` in place). */
38
+ backHref?: string;
39
+ /** Whether `message` currently lives in Sent Items — the only folder recall is offered from, matching
40
+ * Outlook's own restriction (and `BaseMessageRoute.recall()`'s own server-side check). Each caller
41
+ * computes this from its own already-loaded folder list rather than this component fetching folders
42
+ * itself. */
43
+ isSentItems?: boolean;
44
+ /** Called with the server's updated copy (carrying `recallRequestedAt`) after a successful recall, so
45
+ * the caller can patch its own in-memory message/list state — mirrors `mailDetailHooks.ts`'s
46
+ * `useMarkMessageRead`'s identical `onUpdated` callback. */
47
+ onRecalled?: (updated: Message) => void;
48
+ /** Whether `message` currently lives in Outbox — the only folder a scheduled send can be canceled
49
+ * from. Each caller computes this the same way it computes `isSentItems`. */
50
+ isOutbox?: boolean;
51
+ /** The mailbox's Drafts folder uid — required when `isOutbox` is `true`, since canceling a
52
+ * scheduled send moves the message back into Drafts (see `cancelScheduledSend()`'s own doc comment
53
+ * on why clearing `scheduledSendTime` alone doesn't do that). */
54
+ draftsFolderUid?: string;
55
+ /** Called with the server's updated copy (now back in Drafts, `scheduledSendTime` cleared) after
56
+ * successfully canceling a scheduled send. */
57
+ onScheduledSendCanceled?: (updated: Message) => void;
58
+ /** Whether `message` currently lives in the Inbox — Focused/Other classification is an Inbox-only
59
+ * concept (`FocusedInboxUtils.classifyMessage()` short-circuits to Focused for every other folder), so
60
+ * the "Move to Other"/"Move to Focused" control below only renders here, the same
61
+ * each-caller-computes-its-own-folder-type pattern `isSentItems`/`isOutbox` already use. */
62
+ isInbox?: boolean;
63
+ /** Called with the server's updated copy (carrying the new `inferenceClassification`) after a
64
+ * successful classify — mirrors `onRecalled`'s identical shape. */
65
+ onClassified?: (updated: Message) => void;
66
+ /** Called with the server's updated copy after approving/declining a pending delivery/read receipt —
67
+ * mirrors `onRecalled`'s identical shape. No gating prop needed (unlike `isSentItems`/`isOutbox`/
68
+ * `isInbox`): `deliveryReceiptPending`/`readReceiptPending` already live directly on `message` and are
69
+ * only ever `true` on a real delivered copy, so the banner below is self-gating. */
70
+ onReceiptHandled?: (updated: Message) => void;
71
+ /** Called with the server's updated copy (now filed under the mailbox's Archive folder) after a
72
+ * successful archive — mirrors `onRecalled`'s identical shape. Archiving itself is offered for any
73
+ * message except one currently in Drafts or Outbox (mirrors `BaseMessageRoute.archive()`'s own
74
+ * server-side 400 guard); Drafts is detected via `draftsFolderUid` (already passed by every caller
75
+ * for the Outbox-cancel flow) rather than a new prop. */
76
+ onArchived?: (updated: Message) => void;
77
+ /** Every label defined in this message's mailbox, for the label-assignment popover below — each
78
+ * caller fetches its own mailbox's labels the same way it already resolves `draftsFolderUid`
79
+ * (`listLabels()`). Absent/empty simply hides the Labels control - there's nothing to assign. */
80
+ labels?: Label[];
81
+ /** Called with the server's updated copy (carrying the new `labelUids`) after successfully toggling
82
+ * a label — always patches in place, never removes from a caller's list (unlike `onArchived`):
83
+ * changing labels never moves a message between folders. */
84
+ onLabelsChanged?: (updated: Message) => void;
85
+ }
86
+ /**
87
+ * A message's reading pane — header (subject/from/to/attachments) plus a sandboxed iframe for the body.
88
+ * Shared by the desktop inline pane (`apps/www/index.tsx`, always visible alongside the message list),
89
+ * the mobile detail route (`apps/www/messages/[uid].tsx`, a full page on its own reached by tapping
90
+ * a message row), and `ConversationThreadPane` (one per expanded message in a thread) — see each call
91
+ * site for how `message`/`attachments`/`isSentItems` are sourced.
92
+ */
93
+ export default function MessageDetailPane(props: MessageDetailPaneProps): React.JSX.Element;
@@ -0,0 +1,58 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ export interface ComposeSession {
3
+ id: string;
4
+ /** The initial sending mailbox - absent means "the caller's own mailbox" (see `ComposeWindow`). */
5
+ mailboxUid?: string;
6
+ initialTo?: string;
7
+ initialCc?: string;
8
+ initialSubject?: string;
9
+ /** Pre-built HTML (already includes its own quote-attribution wrapper — see `composeQuoting.ts`)
10
+ * inserted below the resolved default signature. Absent for a fresh, non-reply/forward compose. */
11
+ initialQuotedHtml?: string;
12
+ /** Which of a signature's two "default" flags to resolve against — `"new"` (the default) uses
13
+ * `isDefaultForNewMessages`, `"reply_forward"` uses `isDefaultForReplyForward`. */
14
+ signatureContext: "new" | "reply_forward";
15
+ /** See `OpenComposeInput.suppressSigning`'s own doc comment. */
16
+ suppressSigning?: boolean;
17
+ minimized: boolean;
18
+ }
19
+ export interface OpenComposeInput {
20
+ /** The sending mailbox to start with. Omit for a fresh message (defaults to the caller's own mailbox);
21
+ * a reply/forward passes the original message's mailbox so a shared mailbox's mail replies from it.
22
+ * Either way the user can change it via the compose window's From field. */
23
+ mailboxUid?: string;
24
+ /** Prefills the To field — e.g. Contacts' "Email" toolbar action, or Reply/Reply All/Forward. */
25
+ to?: string;
26
+ /** Prefills the Cc field and reveals the Cc/Bcc row — Reply All only. */
27
+ cc?: string;
28
+ /** Prefills the Subject field — Reply/Reply All/Forward. */
29
+ subject?: string;
30
+ /** See `ComposeSession.initialQuotedHtml`'s own doc comment. */
31
+ quotedHtml?: string;
32
+ /** See `ComposeSession.signatureContext`'s own doc comment. Defaults to `"new"` — every existing
33
+ * caller (Contacts' "Email" action, the folder-sidebar "Compose" button) is a fresh compose. */
34
+ signatureContext?: "new" | "reply_forward";
35
+ /** `true` when the caller (`MessageDetailPane.tsx`'s Reply/Reply All) has already determined, via
36
+ * `composeSecurity.ts`'s `isLikelyMailingList()`, that the message being replied to came from a
37
+ * mailing list — a list that appends a footer after signing invalidates the signature (spec's own
38
+ * "Mailing lists" note), so the new compose window defaults its Sign toggle off rather than on.
39
+ * The user can still turn it back on manually; this only changes the *default*. */
40
+ suppressSigning?: boolean;
41
+ }
42
+ export interface ComposeContextValue {
43
+ /** Opens a new compose window, stacked alongside any already open (Gmail allows several at once). */
44
+ openCompose: (input: OpenComposeInput) => void;
45
+ }
46
+ /** Opens the floating Compose window from anywhere inside `AppShell` (any of the four webmail apps). */
47
+ export declare function useCompose(): ComposeContextValue;
48
+ /**
49
+ * Owns every currently-open Compose window and renders them stacked bottom-right, Gmail-style — see
50
+ * `ComposeWindow`'s own doc comment for why this replaced the old dedicated `/compose` page. Mounted
51
+ * once in `AppShell`, so every webmail app (Mail/Calendar/Contacts/Tasks) shares the same instance:
52
+ * opening Compose from Contacts' "Email" action, for instance, overlays the window on top of whatever
53
+ * app is currently showing, exactly like opening it from Mail's own sidebar button.
54
+ */
55
+ export default function ComposeProvider({ children, userUid, trusted }: PropsWithChildren<{
56
+ userUid?: string;
57
+ trusted?: boolean;
58
+ }>): React.JSX.Element;
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import type { Editor } from "@tiptap/react";
3
+ export interface ComposeToolbarProps {
4
+ /** `null` before the editor has mounted client-side (see `RichTextEditor`'s `immediatelyRender: false`
5
+ * doc comment) — every button is disabled in that state rather than the toolbar rendering nothing, so
6
+ * its layout doesn't shift the instant the editor becomes ready. */
7
+ editor: Editor | null;
8
+ /** Uploads `file` as an attachment on the current draft and resolves to a URL the browser can
9
+ * preview it at while composing — see `BaseMailComposeRoute.rewriteInlineImageSources()`'s doc
10
+ * comment for why that's not the final URL the message actually ships with. Resolves to `null`
11
+ * (rather than rejecting) on failure — `ComposeWindow`'s own implementation is responsible for
12
+ * surfacing the error itself, so a failed upload here just quietly doesn't insert an image. */
13
+ onUploadImage: (file: File) => Promise<string | null>;
14
+ }
15
+ export default function ComposeToolbar({ editor, onUploadImage }: ComposeToolbarProps): React.JSX.Element;
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import type { ComposeSession } from "./ComposeContext.js";
3
+ export interface ComposeWindowProps {
4
+ session: ComposeSession;
5
+ onClose: () => void;
6
+ onToggleMinimize: () => void;
7
+ /** The signed-in user - identifies their own ("primary") mailbox, the default From when the session
8
+ * doesn't name a mailbox. */
9
+ userUid?: string;
10
+ /** The caller holds a trusted (admin) role - every mailbox is writable, so no per-mailbox access checks. */
11
+ trusted?: boolean;
12
+ /** How long (ms) after the last edit the draft is autosaved. Defaults to `DEFAULT_AUTOSAVE_DELAY_MS`;
13
+ * only overridden by tests. */
14
+ autosaveDelayMs?: number;
15
+ /** Backoff (ms) between automatic retries of a failed mailbox/encryption-policy load, one entry per retry.
16
+ * Defaults to `DEFAULT_CRYPTO_RETRY_DELAYS_MS`; only overridden by tests. */
17
+ cryptoRetryDelaysMs?: number[];
18
+ }
19
+ /** Debounce between the last edit and the draft autosave. */
20
+ export declare const DEFAULT_AUTOSAVE_DELAY_MS = 2000;
21
+ /** Automatic retries of a failed mailbox/encryption-policy load; after the last one only "Retry" tries again. */
22
+ export declare const DEFAULT_CRYPTO_RETRY_DELAYS_MS: number[];
23
+ /**
24
+ * A floating, Gmail-style compose window — overlays whatever app is currently showing instead of
25
+ * navigating to a dedicated page (see `ComposeContext.tsx`'s doc comment for why: a full-page compose
26
+ * form meant losing your place in the inbox/calendar/contacts view behind it, and every navigation to
27
+ * `/compose` re-ran this whole app's full mailbox/folder resolution waterfall from scratch). Owns its
28
+ * own draft lifecycle end to end (unlike the old page, which read the Drafts folder from `MailShell`'s
29
+ * context — this window can be opened from apps that never mount `MailShell` at all, e.g. Contacts'
30
+ * "Email" action, so it resolves its own Drafts folder from just a `mailboxUid`).
31
+ *
32
+ * Below the `md` breakpoint, a non-minimized session instead renders as a full-screen sheet (there's no
33
+ * room for a floating window, and the click-drag resize handles below become meaningless full-screen) —
34
+ * see `ComposeContext.tsx`'s own doc comment for how that interacts with several sessions being open at
35
+ * once.
36
+ */
37
+ export default function ComposeWindow({ session, onClose, onToggleMinimize, userUid, trusted, autosaveDelayMs, cryptoRetryDelaysMs, }: ComposeWindowProps): React.JSX.Element;
@@ -0,0 +1,14 @@
1
+ import React, { RefObject } from "react";
2
+ export interface EmojiPickerProps {
3
+ anchorRef: RefObject<HTMLElement | null>;
4
+ onSelect: (native: string) => void;
5
+ onClose: () => void;
6
+ }
7
+ /**
8
+ * "Insert emoji" popup — a scrollable grid of every standard unicode emoji, grouped by category (see
9
+ * `emojiData.ts`). Deliberately no search box, unlike `GifPicker`'s — the user's own request only
10
+ * asked for a browsable grid here, and 8 category headers over ~1900 emojis is already a reasonably
11
+ * navigable amount without one. Rendered via `PopoverPortal` — see that component's own doc comment
12
+ * on why (escaping the toolbar's `overflow-hidden` ancestors).
13
+ */
14
+ export default function EmojiPicker({ anchorRef, onSelect, onClose }: EmojiPickerProps): React.JSX.Element;