@rapidmx/web-client 0.3.0 → 0.4.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 (102) hide show
  1. package/apps/admin/branding/index.tsx +39 -404
  2. package/apps/admin/domains/[uid].tsx +92 -259
  3. package/apps/admin/encryption-policy/index.tsx +19 -0
  4. package/apps/admin/index.tsx +100 -82
  5. package/apps/admin/mailbox-policy/index.tsx +19 -0
  6. package/apps/admin/mailboxes/new/index.tsx +28 -291
  7. package/apps/admin/plugins/index.tsx +15 -0
  8. package/apps/admin/retention-policy/index.tsx +39 -132
  9. package/apps/admin/setup/index.tsx +15 -0
  10. package/apps/shared/components/admin/layout/AdminShell.tsx +249 -210
  11. package/apps/shared/components/admin/settings/BrandingForm.tsx +374 -0
  12. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +176 -0
  13. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +104 -0
  14. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
  15. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +302 -0
  16. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +119 -0
  17. package/apps/shared/components/admin/settings/PluginsManager.tsx +595 -0
  18. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +98 -0
  19. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +254 -0
  20. package/apps/shared/components/admin/setup/SetupWizard.tsx +300 -0
  21. package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
  22. package/apps/shared/components/calendar/EventModal.tsx +40 -4
  23. package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
  24. package/apps/shared/components/contacts/ContactForm.tsx +38 -4
  25. package/apps/shared/components/layout/AppShell.tsx +198 -169
  26. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +298 -265
  27. package/apps/shared/components/layout/UnlockPromptProvider.tsx +128 -0
  28. package/apps/shared/components/mail/MessageDetailPane.tsx +630 -595
  29. package/apps/shared/components/mail/compose/ComposeContext.tsx +8 -3
  30. package/apps/shared/components/mail/compose/ComposeWindow.tsx +930 -765
  31. package/apps/shared/components/mail/layout/MailShell.tsx +408 -302
  32. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  33. package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
  34. package/apps/shared/search/LocalIndexLifecycle.tsx +89 -0
  35. package/apps/shared/search/localIndexBlockCipher.ts +83 -0
  36. package/apps/shared/search/localIndexBuilder.ts +179 -0
  37. package/apps/shared/search/localIndexKey.ts +27 -0
  38. package/apps/shared/search/localIndexRpcClient.ts +116 -0
  39. package/apps/shared/search/localIndexSchema.ts +227 -0
  40. package/apps/shared/search/localIndexSizePreference.ts +88 -0
  41. package/apps/shared/search/localIndexVFS.ts +235 -0
  42. package/apps/shared/search/localIndexWorker.ts +464 -0
  43. package/apps/shared/search/searchTier2.ts +79 -0
  44. package/apps/shared/search/wa-sqlite-shims.d.ts +44 -0
  45. package/apps/www/calendar/index.tsx +31 -21
  46. package/apps/www/contacts/index.tsx +33 -6
  47. package/apps/www/index.tsx +661 -106
  48. package/apps/www/messages/[uid].tsx +5 -1
  49. package/apps/www/settings/encryption/index.tsx +63 -1
  50. package/apps/www/settings/sharing/index.tsx +271 -0
  51. package/apps/www/tasks/index.tsx +53 -4
  52. package/dist/apps/admin/branding/index.js +4 -98
  53. package/dist/apps/admin/domains/[uid].js +5 -68
  54. package/dist/apps/admin/encryption-policy/index.js +8 -0
  55. package/dist/apps/admin/index.js +14 -1
  56. package/dist/apps/admin/mailbox-policy/index.js +8 -0
  57. package/dist/apps/admin/mailboxes/new/index.js +4 -89
  58. package/dist/apps/admin/plugins/index.js +6 -0
  59. package/dist/apps/admin/retention-policy/index.js +3 -36
  60. package/dist/apps/admin/setup/index.js +6 -0
  61. package/dist/apps/shared/components/admin/layout/AdminShell.js +34 -3
  62. package/dist/apps/shared/components/admin/settings/BrandingForm.js +105 -0
  63. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +77 -0
  64. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +57 -0
  65. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
  66. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +106 -0
  67. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +52 -0
  68. package/dist/apps/shared/components/admin/settings/PluginsManager.js +260 -0
  69. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +44 -0
  70. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +131 -0
  71. package/dist/apps/shared/components/admin/setup/SetupWizard.js +142 -0
  72. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
  73. package/dist/apps/shared/components/calendar/EventModal.js +16 -4
  74. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
  75. package/dist/apps/shared/components/contacts/ContactForm.js +16 -5
  76. package/dist/apps/shared/components/layout/AppShell.js +30 -7
  77. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +15 -3
  78. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +77 -0
  79. package/dist/apps/shared/components/mail/MessageDetailPane.js +27 -2
  80. package/dist/apps/shared/components/mail/compose/ComposeContext.js +2 -2
  81. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +134 -14
  82. package/dist/apps/shared/components/mail/layout/MailShell.js +106 -40
  83. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  84. package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
  85. package/dist/apps/shared/search/LocalIndexLifecycle.js +76 -0
  86. package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
  87. package/dist/apps/shared/search/localIndexBuilder.js +153 -0
  88. package/dist/apps/shared/search/localIndexKey.js +25 -0
  89. package/dist/apps/shared/search/localIndexRpcClient.js +78 -0
  90. package/dist/apps/shared/search/localIndexSchema.js +179 -0
  91. package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
  92. package/dist/apps/shared/search/localIndexVFS.js +230 -0
  93. package/dist/apps/shared/search/localIndexWorker.js +341 -0
  94. package/dist/apps/shared/search/searchTier2.js +38 -0
  95. package/dist/apps/www/calendar/index.js +14 -12
  96. package/dist/apps/www/contacts/index.js +15 -6
  97. package/dist/apps/www/index.js +496 -94
  98. package/dist/apps/www/messages/[uid].js +5 -1
  99. package/dist/apps/www/settings/encryption/index.js +30 -2
  100. package/dist/apps/www/settings/sharing/index.js +128 -0
  101. package/dist/apps/www/tasks/index.js +27 -5
  102. package/package.json +3 -2
@@ -19,7 +19,6 @@ import {
19
19
  startOfWeek,
20
20
  } from "date-fns";
21
21
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
22
- import { colorForFolder } from "@rapidmx/react-shared/calendar/calendarColors.js";
23
22
  import { CalendarEvent, listCalendarEvents } from "@rapidmx/react-shared/calendar/calendarApi.js";
24
23
  import { moveOccurrence, resizeOccurrenceEnd } from "@rapidmx/react-shared/calendar/calendarMutations.js";
25
24
  import { resolveDragAction } from "@rapidmx/react-shared/calendar/calendarDragIds.js";
@@ -58,10 +57,7 @@ interface ModalState {
58
57
  }
59
58
 
60
59
  function CalendarContent() {
61
- const { mailboxUid, folderUid, calendarFolders, mailboxes, reloadFolders } = useCalendarShell();
62
- // `CalendarShell` only ever renders this component once `mailboxUid` is set, and always to a value
63
- // drawn from `mailboxes` itself (see its own resolution logic) — the lookup below always succeeds.
64
- const organizerAddress = mailboxes.find((mb) => mb.uid === mailboxUid)!.primarySmtpAddress;
60
+ const { mailboxUid, folderUid, calendarFolders, mailboxCalendars, mailboxes, reloadFolders, colorFor } = useCalendarShell();
65
61
  // `PointerSensor` alone activates a drag on the very first touch-move, indistinguishable from a
66
62
  // scroll gesture on a touch device. `MouseSensor` (a small `distance` — desktop drags still start
67
63
  // immediately on a deliberate movement, no change from before) + `TouchSensor` (a `delay`+`tolerance`
@@ -100,8 +96,8 @@ function CalendarContent() {
100
96
 
101
97
  const checkedFolderUids = checkedFolderUidsState ?? new Set(calendarFolders.map((f) => f.uid));
102
98
  const folderColors = useMemo(
103
- () => Object.fromEntries(calendarFolders.map((f) => [f.uid, colorForFolder(f)])),
104
- [calendarFolders],
99
+ () => Object.fromEntries(calendarFolders.map((f) => [f.uid, colorFor(f)])),
100
+ [calendarFolders, colorFor],
105
101
  );
106
102
 
107
103
  function toggleCalendar(toggledUid: string) {
@@ -116,9 +112,8 @@ function CalendarContent() {
116
112
  });
117
113
  }
118
114
 
119
- async function handleAddCalendar(name: string, color: string) {
120
- // `CalendarContent` only renders once `mailboxUid` is resolved — same invariant as `organizerAddress` above.
121
- const created = await createFolder({ mailboxUid: mailboxUid!, name, type: "calendar", color });
115
+ async function handleAddCalendar(targetMailboxUid: string, name: string, color: string) {
116
+ const created = await createFolder({ mailboxUid: targetMailboxUid, name, type: "calendar", color });
122
117
  setCheckedFolderUidsState((prev) => new Set([...(prev ?? calendarFolders.map((f) => f.uid)), created.uid]));
123
118
  reloadFolders();
124
119
  }
@@ -198,8 +193,8 @@ function CalendarContent() {
198
193
  () =>
199
194
  calendarFolders
200
195
  .filter((f) => checkedFolderUids.has(f.uid))
201
- .map((f) => ({ folderUid: f.uid, name: f.name, color: colorForFolder(f) })),
202
- [calendarFolders, checkedFolderUidsKey],
196
+ .map((f) => ({ folderUid: f.uid, name: f.name, color: colorFor(f) })),
197
+ [calendarFolders, checkedFolderUidsKey, colorFor],
203
198
  );
204
199
 
205
200
  function shiftView(direction: 1 | -1) {
@@ -277,14 +272,27 @@ function CalendarContent() {
277
272
  ? `${format(rangeStart, "MMM d")} – ${format(rangeEnd, "MMM d, yyyy")}`
278
273
  : format(viewDate, "EEEE, MMMM d, yyyy");
279
274
 
275
+ const modalFolderUid = modal ? (modal.targetFolderUid ?? modal.occurrence?.folderUid ?? folderUid) : undefined;
276
+ const modalMailboxUid = calendarFolders.find((f) => f.uid === modalFolderUid)?.mailboxUid ?? mailboxUid;
277
+ const modalMailbox = mailboxes.find((mb) => mb.uid === modalMailboxUid);
278
+ // Mailboxes a new event can be created in - only those with at least one calendar.
279
+ const mailboxOptions = useMemo(
280
+ () =>
281
+ mailboxCalendars
282
+ .filter((mc) => mc.calendarFolders.length > 0)
283
+ .map((mc) => ({ mailbox: mc.mailbox, calendars: mc.calendarFolders.map((f) => ({ uid: f.uid, name: f.name })) })),
284
+ [mailboxCalendars],
285
+ );
286
+
280
287
  const sidebarContent = (
281
288
  <>
282
289
  <MiniDatePicker selected={viewDate} onSelect={(date) => setViewDate(startOfDay(date))} />
283
290
  <CalendarListSidebar
284
- calendars={calendarFolders}
291
+ mailboxCalendars={mailboxCalendars}
285
292
  checkedFolderUids={checkedFolderUids}
286
293
  onToggle={toggleCalendar}
287
294
  onAddCalendar={handleAddCalendar}
295
+ colorFor={colorFor}
288
296
  />
289
297
  </>
290
298
  );
@@ -383,17 +391,19 @@ function CalendarContent() {
383
391
  </DndContext>
384
392
  )}
385
393
 
386
- {/* `mailboxUid`/`folderUid` are guaranteed defined whenever `modal` is: `openNewEvent` only sets it
387
- after checking both, and `openEvent` only fires from an occurrence that itself required a
388
- successful, folder-scoped load to render. */}
389
- {modal && (
394
+ {/* `folderUid` is guaranteed defined whenever `modal` is: `openNewEvent` only sets it after
395
+ checking it, and `openEvent` only fires from an occurrence that itself required a
396
+ successful, folder-scoped load to render. The modal is scoped to that folder's own mailbox
397
+ (which may be a shared one) - its organizer address and calendar picker come from there. */}
398
+ {modal && modalMailbox && (
390
399
  <EventModal
391
400
  open
392
401
  onClose={closeModal}
393
- mailboxUid={mailboxUid!}
394
- folderUid={modal.targetFolderUid ?? modal.occurrence?.folderUid ?? folderUid!}
395
- calendars={calendarFolders.map((f) => ({ uid: f.uid, name: f.name }))}
396
- organizerAddress={organizerAddress}
402
+ mailboxUid={modalMailbox.uid}
403
+ folderUid={modalFolderUid!}
404
+ calendars={calendarFolders.filter((f) => f.mailboxUid === modalMailbox.uid).map((f) => ({ uid: f.uid, name: f.name }))}
405
+ mailboxOptions={mailboxOptions}
406
+ organizerAddress={modalMailbox.primarySmtpAddress}
397
407
  occurrence={modal.occurrence}
398
408
  initialStart={modal.initialStart}
399
409
  initialEnd={modal.initialEnd}
@@ -53,7 +53,7 @@ function downloadTextFile(filename: string, content: string): void {
53
53
  }
54
54
 
55
55
  function ContactsContent() {
56
- const { folderUid, mailboxUid } = useContactsShell();
56
+ const { folderUid, mailboxUid, mailboxes } = useContactsShell();
57
57
  const { openCompose } = useCompose();
58
58
  const isMobile = useIsMobile();
59
59
  const [contacts, setContacts] = useState<Contact[]>([]);
@@ -69,6 +69,8 @@ function ContactsContent() {
69
69
  const [checkedUids, setCheckedUids] = useState<Set<string>>(new Set());
70
70
  const [selectedUid, setSelectedUid] = useState<string | null>(null);
71
71
  const [mode, setMode] = useState<Mode>("view");
72
+ // Set after a contact is created in a different mailbox than the one this list shows.
73
+ const [savedElsewhere, setSavedElsewhere] = useState<{ mailboxUid: string; displayName: string } | null>(null);
72
74
 
73
75
  /** Returns a promise so bulk actions (below) can wait for the refreshed list before re-asserting their
74
76
  * own error message — this always clears `error` first (a legitimate reset for a fresh fetch attempt),
@@ -192,8 +194,15 @@ function ContactsContent() {
192
194
  }
193
195
 
194
196
  function handleSaved(contact: Contact) {
195
- setSelectedUid(contact.uid);
196
197
  setMode("view");
198
+ if (contact.mailboxUid !== mailboxUid) {
199
+ // Created in another mailbox (via the form's Mailbox selector) - it won't be in this list.
200
+ setSelectedUid(null);
201
+ setSavedElsewhere({ mailboxUid: contact.mailboxUid, displayName: contact.displayName });
202
+ return;
203
+ }
204
+ setSavedElsewhere(null);
205
+ setSelectedUid(contact.uid);
197
206
  void reload();
198
207
  }
199
208
 
@@ -237,9 +246,9 @@ function ContactsContent() {
237
246
 
238
247
  function handleEmail() {
239
248
  const addresses = checkedContacts.map((c) => c.emails[0]?.address).filter((a): a is string => Boolean(a));
240
- // `ContactsShell` only ever renders this component once `mailboxUid` is resolved (see its own
241
- // invariant comment) — same non-null pattern as `organizerAddress` in the calendar page.
242
- openCompose({ mailboxUid: mailboxUid!, to: addresses.join(", ") });
249
+ // No mailboxUid: a fresh message defaults to the caller's own mailbox, not whichever mailbox's
250
+ // contacts happen to be open - the compose window's From field can still change it.
251
+ openCompose({ to: addresses.join(", ") });
243
252
  }
244
253
 
245
254
  async function handleToggleFavorite() {
@@ -421,8 +430,26 @@ function ContactsContent() {
421
430
  see handleSelectRow), so this pane only needs to show there for "new", which stays in-place
422
431
  on every device (an unsaved contact has no uid for a route). Always visible on desktop. */}
423
432
  <div className={["flex-1 min-w-0 overflow-y-auto p-6 md:block", mode === "new" ? "block" : "hidden"].join(" ")}>
433
+ {savedElsewhere && mode !== "new" && (
434
+ <p role="status" className="mb-4 text-sm py-2 px-3 rounded-sm bg-surface-alt text-text">
435
+ {savedElsewhere.displayName} was added to{" "}
436
+ {mailboxes.find((mb) => mb.uid === savedElsewhere.mailboxUid)?.displayName ?? "another mailbox"}.{" "}
437
+ <a
438
+ href={`/contacts?mailboxUid=${encodeURIComponent(savedElsewhere.mailboxUid)}`}
439
+ className="font-medium text-primary-dark hover:underline"
440
+ >
441
+ View that mailbox&rsquo;s contacts
442
+ </a>
443
+ </p>
444
+ )}
424
445
  {mode === "new" && mailboxUid && folderUid ? (
425
- <ContactForm mailboxUid={mailboxUid} folderUid={folderUid} onSaved={handleSaved} onCancel={handleCancel} />
446
+ <ContactForm
447
+ mailboxUid={mailboxUid}
448
+ folderUid={folderUid}
449
+ mailboxes={mailboxes}
450
+ onSaved={handleSaved}
451
+ onCancel={handleCancel}
452
+ />
426
453
  ) : mode === "edit" && selected ? (
427
454
  <ContactForm contact={selected} onSaved={handleSaved} onCancel={handleCancel} />
428
455
  ) : selected ? (