@rapidmx/web-client 0.5.0 → 0.7.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 (224) hide show
  1. package/README.md +88 -0
  2. package/apps/shared/components/admin/layout/AdminShell.tsx +25 -8
  3. package/apps/shared/components/admin/settings/BrandingForm.tsx +398 -385
  4. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -112
  5. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +159 -152
  6. package/apps/shared/components/admin/settings/PluginsManager.tsx +1288 -1235
  7. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +9 -1
  8. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -290
  9. package/apps/shared/components/admin/setup/SetupWizard.tsx +443 -416
  10. package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
  11. package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
  12. package/apps/shared/components/layout/AppShell.tsx +288 -270
  13. package/apps/shared/components/mail/ConversationList.tsx +225 -33
  14. package/apps/shared/components/mail/ConversationThreadPane.tsx +434 -216
  15. package/apps/shared/components/mail/MailListToolbar.tsx +265 -0
  16. package/apps/shared/components/mail/MailSelectionBar.tsx +222 -0
  17. package/apps/shared/components/mail/MenuButton.tsx +384 -0
  18. package/apps/shared/components/mail/MessageDetailPane.tsx +280 -215
  19. package/apps/shared/components/mail/MoveToFolderDialog.tsx +263 -0
  20. package/apps/shared/components/mail/compose/ComposeContext.tsx +29 -2
  21. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1700 -1652
  22. package/apps/shared/components/mail/compose/RecipientInput.tsx +356 -0
  23. package/apps/shared/components/mail/compose/RichTextEditor.tsx +38 -3
  24. package/apps/shared/components/mail/compose/quotedBody.ts +100 -0
  25. package/apps/shared/components/mail/compose/recipients.ts +104 -0
  26. package/apps/shared/components/mail/labelMenu.tsx +278 -0
  27. package/apps/shared/components/mail/layout/MailShell.tsx +31 -4
  28. package/apps/shared/components/mail/listPreferences.ts +228 -0
  29. package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -10
  30. package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
  31. package/apps/shared/plugins/pluginNav.ts +73 -0
  32. package/apps/shared/search/localIndexRpcClient.ts +3 -1
  33. package/apps/shared/styles/app.css +28 -5
  34. package/apps/www/index.tsx +1854 -1314
  35. package/apps/www/messages/[uid].tsx +5 -4
  36. package/dist/apps/admin/_404.d.ts +2 -0
  37. package/dist/apps/admin/_500.d.ts +2 -0
  38. package/dist/apps/admin/_layout.d.ts +8 -0
  39. package/dist/apps/admin/audit-log/index.d.ts +11 -0
  40. package/dist/apps/admin/branding/index.d.ts +3 -0
  41. package/dist/apps/admin/data-requests/index.d.ts +3 -0
  42. package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
  43. package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
  44. package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
  45. package/dist/apps/admin/domains/[uid].d.ts +7 -0
  46. package/dist/apps/admin/domains/index.d.ts +3 -0
  47. package/dist/apps/admin/domains/new/index.d.ts +3 -0
  48. package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
  49. package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
  50. package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
  51. package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
  52. package/dist/apps/admin/index.d.ts +3 -0
  53. package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
  54. package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
  55. package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
  56. package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
  57. package/dist/apps/admin/plugins/index.d.ts +3 -0
  58. package/dist/apps/admin/quarantine/index.d.ts +6 -0
  59. package/dist/apps/admin/retention-policy/index.d.ts +3 -0
  60. package/dist/apps/admin/setup/index.d.ts +3 -0
  61. package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
  62. package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
  63. package/dist/apps/admin/transport-rules/index.d.ts +3 -0
  64. package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
  65. package/dist/apps/escrow/_layout.d.ts +10 -0
  66. package/dist/apps/escrow/audit-log/index.d.ts +9 -0
  67. package/dist/apps/escrow/index.d.ts +3 -0
  68. package/dist/apps/escrow/matters/[uid].d.ts +7 -0
  69. package/dist/apps/escrow/matters/new/index.d.ts +3 -0
  70. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
  71. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
  72. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
  73. package/dist/apps/shared/components/admin/layout/AdminShell.js +12 -5
  74. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
  75. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
  76. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
  77. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
  78. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +13 -0
  79. package/dist/apps/shared/components/admin/settings/BrandingForm.js +6 -5
  80. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
  81. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +15 -0
  82. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +2 -2
  83. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
  84. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
  85. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +13 -0
  86. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +2 -2
  87. package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +11 -0
  88. package/dist/apps/shared/components/admin/settings/PluginsManager.js +33 -14
  89. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +12 -0
  90. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +2 -2
  91. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
  92. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +1 -1
  93. package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
  94. package/dist/apps/shared/components/admin/setup/SetupWizard.js +14 -10
  95. package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
  96. package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
  97. package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
  98. package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
  99. package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
  100. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
  101. package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
  102. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
  103. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
  104. package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
  105. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
  106. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +2 -2
  107. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
  108. package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
  109. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
  110. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
  111. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
  112. package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
  113. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
  114. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
  115. package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
  116. package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
  117. package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
  118. package/dist/apps/shared/components/layout/AppShell.js +15 -13
  119. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
  120. package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
  121. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
  122. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
  123. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
  124. package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
  125. package/dist/apps/shared/components/mail/ConversationList.d.ts +47 -0
  126. package/dist/apps/shared/components/mail/ConversationList.js +84 -10
  127. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +62 -0
  128. package/dist/apps/shared/components/mail/ConversationThreadPane.js +217 -60
  129. package/dist/apps/shared/components/mail/MailListToolbar.d.ts +55 -0
  130. package/dist/apps/shared/components/mail/MailListToolbar.js +149 -0
  131. package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +59 -0
  132. package/dist/apps/shared/components/mail/MailSelectionBar.js +48 -0
  133. package/dist/apps/shared/components/mail/MenuButton.d.ts +71 -0
  134. package/dist/apps/shared/components/mail/MenuButton.js +211 -0
  135. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +101 -0
  136. package/dist/apps/shared/components/mail/MessageDetailPane.js +126 -129
  137. package/dist/apps/shared/components/mail/MoveToFolderDialog.d.ts +57 -0
  138. package/dist/apps/shared/components/mail/MoveToFolderDialog.js +138 -0
  139. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +73 -0
  140. package/dist/apps/shared/components/mail/compose/ComposeContext.js +3 -1
  141. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
  142. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
  143. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +52 -20
  144. package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
  145. package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
  146. package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +36 -0
  147. package/dist/apps/shared/components/mail/compose/RecipientInput.js +203 -0
  148. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +43 -0
  149. package/dist/apps/shared/components/mail/compose/RichTextEditor.js +21 -3
  150. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
  151. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
  152. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +37 -0
  153. package/dist/apps/shared/components/mail/compose/quotedBody.js +85 -0
  154. package/dist/apps/shared/components/mail/compose/recipients.d.ts +24 -0
  155. package/dist/apps/shared/components/mail/compose/recipients.js +87 -0
  156. package/dist/apps/shared/components/mail/labelMenu.d.ts +97 -0
  157. package/dist/apps/shared/components/mail/labelMenu.js +134 -0
  158. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +65 -0
  159. package/dist/apps/shared/components/mail/layout/MailShell.js +17 -5
  160. package/dist/apps/shared/components/mail/listPreferences.d.ts +101 -0
  161. package/dist/apps/shared/components/mail/listPreferences.js +186 -0
  162. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
  163. package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
  164. package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
  165. package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
  166. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
  167. package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -10
  168. package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
  169. package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
  170. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
  171. package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
  172. package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
  173. package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
  174. package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
  175. package/dist/apps/shared/plugins/pluginNav.js +33 -0
  176. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
  177. package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
  178. package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
  179. package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
  180. package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
  181. package/dist/apps/shared/search/localIndexRpcClient.js +3 -1
  182. package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
  183. package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
  184. package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
  185. package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
  186. package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
  187. package/dist/apps/shared/search/searchTier2.d.ts +30 -0
  188. package/dist/apps/shared/styles/app.css +28 -5
  189. package/dist/apps/www/_404.d.ts +2 -0
  190. package/dist/apps/www/_500.d.ts +2 -0
  191. package/dist/apps/www/_layout.d.ts +14 -0
  192. package/dist/apps/www/calendar/index.d.ts +3 -0
  193. package/dist/apps/www/contacts/[uid].d.ts +13 -0
  194. package/dist/apps/www/contacts/index.d.ts +3 -0
  195. package/dist/apps/www/index.d.ts +3 -0
  196. package/dist/apps/www/index.js +506 -112
  197. package/dist/apps/www/messages/[uid].d.ts +11 -0
  198. package/dist/apps/www/messages/[uid].js +2 -3
  199. package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
  200. package/dist/apps/www/settings/encryption/index.d.ts +7 -0
  201. package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
  202. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
  203. package/dist/apps/www/settings/filters/index.d.ts +4 -0
  204. package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
  205. package/dist/apps/www/settings/labels/index.d.ts +4 -0
  206. package/dist/apps/www/settings/privacy/index.d.ts +4 -0
  207. package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
  208. package/dist/apps/www/settings/sharing/index.d.ts +4 -0
  209. package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
  210. package/dist/apps/www/settings/signatures/index.d.ts +4 -0
  211. package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
  212. package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
  213. package/dist/apps/www/tasks/index.d.ts +3 -0
  214. package/package.json +3 -3
  215. package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
  216. package/apps/www/settings/booking-types/[uid].tsx +0 -306
  217. package/apps/www/settings/booking-types/index.tsx +0 -98
  218. package/apps/www/settings/booking-types/new/index.tsx +0 -212
  219. package/apps/www/settings/focused-inbox/index.tsx +0 -150
  220. package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
  221. package/dist/apps/www/settings/booking-types/[uid].js +0 -134
  222. package/dist/apps/www/settings/booking-types/index.js +0 -29
  223. package/dist/apps/www/settings/booking-types/new/index.js +0 -86
  224. package/dist/apps/www/settings/focused-inbox/index.js +0 -71
@@ -1,197 +1,199 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
6
- import { HiOutlineBars3 } from "react-icons/hi2";
7
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
- import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
9
- import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
10
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
- import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
- import AppShell, { AppShellProps } from "../../layout/AppShell.js";
13
- import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
14
-
15
- export type ContactsShellProps = Omit<AppShellProps, "active">;
16
-
17
- export interface ContactsShellContextValue {
18
- /** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox). */
19
- mailboxUid?: string;
20
- /** The selected mailbox's single `contacts`-type folder. */
21
- folderUid?: string;
22
- mailboxes: Mailbox[];
23
- }
24
-
25
- const ContactsShellContext = createContext<ContactsShellContextValue>({ mailboxes: [] });
26
-
27
- /** Reads the mailbox/folder Contacts is currently showing, as resolved by the enclosing `ContactsShell`. */
28
- export function useContactsShell(): ContactsShellContextValue {
29
- return useContext(ContactsShellContext);
30
- }
31
-
32
- type Status = "checking" | "error" | "ready";
33
-
34
- /**
35
- * The Contacts app's shell: unlike `MailShell` (a folder tree, since a mailbox has many mail folders),
36
- * a mailbox has exactly one well-known `contacts` folder — see `BaseMailboxRoute.create()`'s eager
37
- * provisioning in `@rapidmx/restapi`, which guarantees it exists for every mailbox. This shell's own
38
- * sidebar is therefore just a mailbox switcher (when the caller has more than one); the actual contact
39
- * list/detail UI is `apps/www/contacts/index.tsx`'s own content, rendered as `children` here.
40
- */
41
- export default function ContactsShell({
42
- userUid,
43
- authServerUrl,
44
- impersonating,
45
- impersonationBaseUrl,
46
- trusted,
47
- children,
48
- }: PropsWithChildren<ContactsShellProps>) {
49
- const [status, setStatus] = useState<Status>("checking");
50
- const [error, setError] = useState<string | null>(null);
51
- const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
52
- const [folders, setFolders] = useState<Folder[]>([]);
53
- const [folderError, setFolderError] = useState<string | null>(null);
54
- const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
55
- const [drawerOpen, setDrawerOpen] = useState(false);
56
-
57
- useEffect(() => {
58
- setRequestedMailboxUid(new URLSearchParams(window.location.search).get("mailboxUid"));
59
- }, []);
60
-
61
- useEffect(() => {
62
- if (!userUid) {
63
- return;
64
- }
65
- listMailboxes({ limit: 100 })
66
- .then((result) => {
67
- setMailboxes(result);
68
- setStatus("ready");
69
- })
70
- .catch((err) => {
71
- setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
72
- setStatus("error");
73
- });
74
- }, [userUid]);
75
-
76
- const mailboxUid: string | undefined =
77
- (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
78
- mailboxes[0]?.uid;
79
-
80
- useEffect(() => {
81
- if (!mailboxUid) {
82
- setFolders([]);
83
- return;
84
- }
85
- setFolderError(null);
86
- listFolders(mailboxUid)
87
- .then(setFolders)
88
- .catch((err) => setFolderError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox's contacts folder."));
89
- }, [mailboxUid]);
90
-
91
- const folderUid: string | undefined = folders.find((f) => f.type === "contacts")?.uid;
92
-
93
- const contextValue = useMemo<ContactsShellContextValue>(
94
- () => ({ mailboxUid, folderUid, mailboxes }),
95
- [mailboxUid, folderUid, mailboxes],
96
- );
97
-
98
- // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
99
- // for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
100
- if (userUid && status === "ready" && !mailboxUid) {
101
- return <MailboxProvisioning />;
102
- }
103
-
104
- let inner: ReactNode = null;
105
- if (userUid && status === "checking") {
106
- // Renders immediately (no network round trip needed) so switching into Contacts never shows a
107
- // blank pane while `listMailboxes()` is in flight — the real sidebar (favorites/lists/categories)
108
- // lives in `apps/www/contacts/index.tsx`'s own content, so this mimics its rough shape rather
109
- // than the (minimal, mailbox-switcher-only) shape of this shell's own sidebar.
110
- inner = (
111
- <div className="w-56 shrink-0 border-r border-border flex flex-col gap-4 p-3">
112
- <Skeleton height="h-9" />
113
- <SkeletonList count={5} />
114
- </div>
115
- );
116
- } else if (userUid && status === "error") {
117
- inner = (
118
- <div className="flex-1 flex items-center justify-center p-8">
119
- <div className="w-full max-w-md">
120
- <Alert>{error}</Alert>
121
- </div>
122
- </div>
123
- );
124
- } else if (userUid && status === "ready") {
125
- // A function, not a plain JSX constant — see MailShell's identical comment: it's rendered twice
126
- // (desktop `<aside>` + mobile `Drawer`), possibly simultaneously mounted, so the `<select>`'s
127
- // `id`/its `<label>`'s `htmlFor` need a distinct value per instance to avoid a duplicate-id
128
- // label-association break.
129
- const sidebarContent = (idPrefix: string) => (
130
- <>
131
- {mailboxes.length > 1 && (
132
- <div>
133
- <label
134
- className="block text-xs font-bold uppercase tracking-wide text-text-muted mb-1"
135
- htmlFor={`${idPrefix}-contacts-mailbox-switcher`}
136
- >
137
- Mailbox
138
- </label>
139
- <select
140
- id={`${idPrefix}-contacts-mailbox-switcher`}
141
- className="w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface"
142
- value={mailboxUid}
143
- onChange={(e) => {
144
- window.location.href = `/contacts?mailboxUid=${encodeURIComponent(e.target.value)}`;
145
- }}
146
- >
147
- {mailboxes.map((mb) => (
148
- <option key={mb.uid} value={mb.uid}>
149
- {mb.displayName}
150
- {mb.ownerUserUid ? "" : " (shared)"}
151
- </option>
152
- ))}
153
- </select>
154
- </div>
155
- )}
156
- {folderError && <Alert>{folderError}</Alert>}
157
- </>
158
- );
159
-
160
- inner = (
161
- <>
162
- <aside className="hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3">
163
- {sidebarContent("desktop")}
164
- </aside>
165
- <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Mailbox">
166
- <div className="flex flex-col gap-3">{sidebarContent("mobile")}</div>
167
- </Drawer>
168
- <main className="flex-1 min-w-0 flex flex-col">
169
- {(mailboxes.length > 1 || folderError) && (
170
- <button
171
- type="button"
172
- className="md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
173
- aria-label="Open mailbox switcher"
174
- onClick={() => setDrawerOpen(true)}
175
- >
176
- <HiOutlineBars3 size={20} aria-hidden="true" />
177
- </button>
178
- )}
179
- <ContactsShellContext.Provider value={contextValue}>{children}</ContactsShellContext.Provider>
180
- </main>
181
- </>
182
- );
183
- }
184
-
185
- return (
186
- <AppShell
187
- active="contacts"
188
- userUid={userUid}
189
- authServerUrl={authServerUrl}
190
- impersonating={impersonating}
191
- impersonationBaseUrl={impersonationBaseUrl}
192
- trusted={trusted}
193
- >
194
- {inner}
195
- </AppShell>
196
- );
197
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { createContext, PropsWithChildren, ReactNode, useContext, useEffect, useMemo, useState } from "react";
6
+ import { HiOutlineBars3 } from "react-icons/hi2";
7
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
+ import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
9
+ import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
10
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
+ import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
12
+ import AppShell, { AppShellProps } from "../../layout/AppShell.js";
13
+ import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
14
+
15
+ export type ContactsShellProps = Omit<AppShellProps, "active">;
16
+
17
+ export interface ContactsShellContextValue {
18
+ /** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox). */
19
+ mailboxUid?: string;
20
+ /** The selected mailbox's single `contacts`-type folder. */
21
+ folderUid?: string;
22
+ mailboxes: Mailbox[];
23
+ }
24
+
25
+ const ContactsShellContext = createContext<ContactsShellContextValue>({ mailboxes: [] });
26
+
27
+ /** Reads the mailbox/folder Contacts is currently showing, as resolved by the enclosing `ContactsShell`. */
28
+ export function useContactsShell(): ContactsShellContextValue {
29
+ return useContext(ContactsShellContext);
30
+ }
31
+
32
+ type Status = "checking" | "error" | "ready";
33
+
34
+ /**
35
+ * The Contacts app's shell: unlike `MailShell` (a folder tree, since a mailbox has many mail folders),
36
+ * a mailbox has exactly one well-known `contacts` folder — see `BaseMailboxRoute.create()`'s eager
37
+ * provisioning in `@rapidmx/restapi`, which guarantees it exists for every mailbox. This shell's own
38
+ * sidebar is therefore just a mailbox switcher (when the caller has more than one); the actual contact
39
+ * list/detail UI is `apps/www/contacts/index.tsx`'s own content, rendered as `children` here.
40
+ */
41
+ export default function ContactsShell({
42
+ userUid,
43
+ authServerUrl,
44
+ impersonating,
45
+ impersonationBaseUrl,
46
+ trusted,
47
+ pluginNav,
48
+ children,
49
+ }: PropsWithChildren<ContactsShellProps>) {
50
+ const [status, setStatus] = useState<Status>("checking");
51
+ const [error, setError] = useState<string | null>(null);
52
+ const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
53
+ const [folders, setFolders] = useState<Folder[]>([]);
54
+ const [folderError, setFolderError] = useState<string | null>(null);
55
+ const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
56
+ const [drawerOpen, setDrawerOpen] = useState(false);
57
+
58
+ useEffect(() => {
59
+ setRequestedMailboxUid(new URLSearchParams(window.location.search).get("mailboxUid"));
60
+ }, []);
61
+
62
+ useEffect(() => {
63
+ if (!userUid) {
64
+ return;
65
+ }
66
+ listMailboxes({ limit: 100 })
67
+ .then((result) => {
68
+ setMailboxes(result);
69
+ setStatus("ready");
70
+ })
71
+ .catch((err) => {
72
+ setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
73
+ setStatus("error");
74
+ });
75
+ }, [userUid]);
76
+
77
+ const mailboxUid: string | undefined =
78
+ (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
79
+ mailboxes[0]?.uid;
80
+
81
+ useEffect(() => {
82
+ if (!mailboxUid) {
83
+ setFolders([]);
84
+ return;
85
+ }
86
+ setFolderError(null);
87
+ listFolders(mailboxUid)
88
+ .then(setFolders)
89
+ .catch((err) => setFolderError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox's contacts folder."));
90
+ }, [mailboxUid]);
91
+
92
+ const folderUid: string | undefined = folders.find((f) => f.type === "contacts")?.uid;
93
+
94
+ const contextValue = useMemo<ContactsShellContextValue>(
95
+ () => ({ mailboxUid, folderUid, mailboxes }),
96
+ [mailboxUid, folderUid, mailboxes],
97
+ );
98
+
99
+ // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
100
+ // for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
101
+ if (userUid && status === "ready" && !mailboxUid) {
102
+ return <MailboxProvisioning />;
103
+ }
104
+
105
+ let inner: ReactNode = null;
106
+ if (userUid && status === "checking") {
107
+ // Renders immediately (no network round trip needed) so switching into Contacts never shows a
108
+ // blank pane while `listMailboxes()` is in flight — the real sidebar (favorites/lists/categories)
109
+ // lives in `apps/www/contacts/index.tsx`'s own content, so this mimics its rough shape rather
110
+ // than the (minimal, mailbox-switcher-only) shape of this shell's own sidebar.
111
+ inner = (
112
+ <div className="w-56 shrink-0 border-r border-border flex flex-col gap-4 p-3">
113
+ <Skeleton height="h-9" />
114
+ <SkeletonList count={5} />
115
+ </div>
116
+ );
117
+ } else if (userUid && status === "error") {
118
+ inner = (
119
+ <div className="flex-1 flex items-center justify-center p-8">
120
+ <div className="w-full max-w-md">
121
+ <Alert>{error}</Alert>
122
+ </div>
123
+ </div>
124
+ );
125
+ } else if (userUid && status === "ready") {
126
+ // A function, not a plain JSX constant — see MailShell's identical comment: it's rendered twice
127
+ // (desktop `<aside>` + mobile `Drawer`), possibly simultaneously mounted, so the `<select>`'s
128
+ // `id`/its `<label>`'s `htmlFor` need a distinct value per instance to avoid a duplicate-id
129
+ // label-association break.
130
+ const sidebarContent = (idPrefix: string) => (
131
+ <>
132
+ {mailboxes.length > 1 && (
133
+ <div>
134
+ <label
135
+ className="block text-xs font-bold uppercase tracking-wide text-text-muted mb-1"
136
+ htmlFor={`${idPrefix}-contacts-mailbox-switcher`}
137
+ >
138
+ Mailbox
139
+ </label>
140
+ <select
141
+ id={`${idPrefix}-contacts-mailbox-switcher`}
142
+ className="w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface"
143
+ value={mailboxUid}
144
+ onChange={(e) => {
145
+ window.location.href = `/contacts?mailboxUid=${encodeURIComponent(e.target.value)}`;
146
+ }}
147
+ >
148
+ {mailboxes.map((mb) => (
149
+ <option key={mb.uid} value={mb.uid}>
150
+ {mb.displayName}
151
+ {mb.ownerUserUid ? "" : " (shared)"}
152
+ </option>
153
+ ))}
154
+ </select>
155
+ </div>
156
+ )}
157
+ {folderError && <Alert>{folderError}</Alert>}
158
+ </>
159
+ );
160
+
161
+ inner = (
162
+ <>
163
+ <aside className="hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3">
164
+ {sidebarContent("desktop")}
165
+ </aside>
166
+ <Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Mailbox">
167
+ <div className="flex flex-col gap-3">{sidebarContent("mobile")}</div>
168
+ </Drawer>
169
+ <main className="flex-1 min-w-0 flex flex-col">
170
+ {(mailboxes.length > 1 || folderError) && (
171
+ <button
172
+ type="button"
173
+ className="md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
174
+ aria-label="Open mailbox switcher"
175
+ onClick={() => setDrawerOpen(true)}
176
+ >
177
+ <HiOutlineBars3 size={20} aria-hidden="true" />
178
+ </button>
179
+ )}
180
+ <ContactsShellContext.Provider value={contextValue}>{children}</ContactsShellContext.Provider>
181
+ </main>
182
+ </>
183
+ );
184
+ }
185
+
186
+ return (
187
+ <AppShell
188
+ active="contacts"
189
+ userUid={userUid}
190
+ authServerUrl={authServerUrl}
191
+ impersonating={impersonating}
192
+ impersonationBaseUrl={impersonationBaseUrl}
193
+ trusted={trusted}
194
+ pluginNav={pluginNav}
195
+ >
196
+ {inner}
197
+ </AppShell>
198
+ );
199
+ }